securewithtrace 0.44.0 → 0.45.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 +1 -1
- package/bin/tracecli +50 -0
- package/dist/index.js +1289 -421
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -22640,7 +22640,11 @@ var agentSessionContract = c2.router({
|
|
|
22640
22640
|
});
|
|
22641
22641
|
|
|
22642
22642
|
// ../../packages/api-contracts/src/schemas/api/v1/agent-task.api.ts
|
|
22643
|
-
var AGENT_TASK_TYPES = [
|
|
22643
|
+
var AGENT_TASK_TYPES = [
|
|
22644
|
+
"ENGAGEMENT_BRIEF",
|
|
22645
|
+
"VERIFY_APPLICATION_SETUP",
|
|
22646
|
+
"TRIAGE_DISTILL"
|
|
22647
|
+
];
|
|
22644
22648
|
var AgentTaskTypeSchema = external_exports.enum(AGENT_TASK_TYPES);
|
|
22645
22649
|
var TRIGGERABLE_AGENT_TASK_TYPES = ["ENGAGEMENT_BRIEF"];
|
|
22646
22650
|
var TriggerableAgentTaskTypeSchema = external_exports.enum(TRIGGERABLE_AGENT_TASK_TYPES);
|
|
@@ -22701,6 +22705,7 @@ var AgentTaskResponseSchema = external_exports.object({
|
|
|
22701
22705
|
status: AgentTaskStatusSchema,
|
|
22702
22706
|
subjectPentestId: external_exports.uuid().nullable(),
|
|
22703
22707
|
subjectApplicationId: external_exports.uuid().nullable(),
|
|
22708
|
+
subjectVulnerabilityId: external_exports.uuid().nullable(),
|
|
22704
22709
|
capabilities: external_exports.array(external_exports.string()),
|
|
22705
22710
|
/**
|
|
22706
22711
|
* Null until the worker claims the task and records what it actually ran —
|
|
@@ -23178,10 +23183,22 @@ var RepositoryResponseSchema = external_exports.object({
|
|
|
23178
23183
|
primaryLanguage: external_exports.string().nullable().optional(),
|
|
23179
23184
|
/** Staff-set opt-in: automated PR scans run on this repo even without a FULL SAST baseline. */
|
|
23180
23185
|
prScansEnabled: external_exports.boolean(),
|
|
23186
|
+
/** Customer-set gate: false = no scans of any kind run for this repo until it is included again. */
|
|
23187
|
+
includedInScanning: external_exports.boolean(),
|
|
23181
23188
|
createdAt: external_exports.date(),
|
|
23182
23189
|
updatedAt: external_exports.date()
|
|
23183
23190
|
});
|
|
23184
23191
|
var ListRepositoriesResponseSchema = external_exports.array(RepositoryResponseSchema);
|
|
23192
|
+
var SetScanningInclusionRequestSchema = external_exports.object({
|
|
23193
|
+
repositoryIds: external_exports.array(external_exports.uuid()).min(1).max(500),
|
|
23194
|
+
includedInScanning: external_exports.boolean()
|
|
23195
|
+
});
|
|
23196
|
+
var SetScanningInclusionResponseSchema = external_exports.object({
|
|
23197
|
+
/** Rows whose flag now matches the request (already-matching rows count). */
|
|
23198
|
+
updatedCount: external_exports.number().int().min(0),
|
|
23199
|
+
/** IDs from the request that are not repositories of this organization. */
|
|
23200
|
+
notFoundRepositoryIds: external_exports.array(external_exports.uuid())
|
|
23201
|
+
});
|
|
23185
23202
|
var SetPrScansEnabledRequestSchema = external_exports.object({
|
|
23186
23203
|
enabled: external_exports.boolean()
|
|
23187
23204
|
});
|
|
@@ -23666,6 +23683,7 @@ var ReportPdfResponseSchema = external_exports.object({
|
|
|
23666
23683
|
var GetReportPdfResponseSchema = external_exports.object({
|
|
23667
23684
|
report: ReportPdfResponseSchema.nullable()
|
|
23668
23685
|
});
|
|
23686
|
+
var CvssVersionSchema = external_exports.enum(["3.1", "4.0"]);
|
|
23669
23687
|
var ReportCanvasFindingSchema = external_exports.object({
|
|
23670
23688
|
pentestFindingId: external_exports.uuid(),
|
|
23671
23689
|
displayId: external_exports.int().min(1),
|
|
@@ -23819,6 +23837,12 @@ var ReportCanvasResponseSchema = external_exports.object({
|
|
|
23819
23837
|
* project the default (true) — the value the first render will use. */
|
|
23820
23838
|
includeRemediationTimeline: external_exports.boolean(),
|
|
23821
23839
|
findings: external_exports.array(ReportCanvasFindingSchema),
|
|
23840
|
+
/** The CVSS version this engagement scores with (its stored vectors, else the
|
|
23841
|
+
* producer default). The editor renders this version's metric grid; the api
|
|
23842
|
+
* rejects the other version on every write, so no finding can differ from it.
|
|
23843
|
+
* Optional for the same reason as `boundApplications` above; consumers default
|
|
23844
|
+
* it to the producer default, which is what an api that predates it scored with. */
|
|
23845
|
+
cvssVersion: CvssVersionSchema.optional(),
|
|
23822
23846
|
// Counts reuse the module-local Severity/Status counts + totalFindings trio.
|
|
23823
23847
|
liveSeverityCounts: SeverityCountsSchema,
|
|
23824
23848
|
liveStatusCounts: StatusCountsSchema,
|
|
@@ -24017,6 +24041,11 @@ var IssuedReportSnapshotSchema = external_exports.object({
|
|
|
24017
24041
|
* parses those rows for the changelog diff — a required field would throw
|
|
24018
24042
|
* there and break issuing a new version of any pre-feature report. */
|
|
24019
24043
|
testType: PentestTestType.optional(),
|
|
24044
|
+
/** The CVSS version the engagement was scored with at issuance: the archival
|
|
24045
|
+
* record of which version the delivered PDF names when its rendered findings
|
|
24046
|
+
* carry no vector to read it from. Optional because every pre-feature snapshot
|
|
24047
|
+
* lacks it, and those were all issued under 3.1. */
|
|
24048
|
+
cvssVersion: CvssVersionSchema.optional(),
|
|
24020
24049
|
/** Types of the applications bound at issuance: the archival record of which
|
|
24021
24050
|
* OWASP families the delivered PDF named. Optional for the same reason as
|
|
24022
24051
|
* `testType` above: a pre-feature snapshot has no value, and
|
|
@@ -24813,8 +24842,11 @@ var FindingProseSchema = FindingRewriteJsonSchema.pick({
|
|
|
24813
24842
|
remediation: true,
|
|
24814
24843
|
verification: true
|
|
24815
24844
|
});
|
|
24845
|
+
var FindingProsePatchSchema = FindingProseSchema.partial().extend({
|
|
24846
|
+
summary: FindingRewriteJsonSchema.shape.summary.unwrap().optional()
|
|
24847
|
+
});
|
|
24816
24848
|
var ApplyFindingProseRequestSchema = external_exports.object({
|
|
24817
|
-
prose:
|
|
24849
|
+
prose: FindingProsePatchSchema,
|
|
24818
24850
|
/**
|
|
24819
24851
|
* Optimistic precondition — the finding's `updatedAt` at edit-open. A mismatch
|
|
24820
24852
|
* means a concurrent writer (staff / report agent / retest) moved the row →
|
|
@@ -24836,7 +24868,9 @@ var CreatePentestFindingRequestSchema = external_exports.object({
|
|
|
24836
24868
|
category: external_exports.string().min(1).max(200),
|
|
24837
24869
|
status: PentestFindingStatus.optional(),
|
|
24838
24870
|
cweIds: external_exports.array(external_exports.string()).optional(),
|
|
24871
|
+
/** `CVSS:3.1/…` or `CVSS:4.0/…`; scored server-side and checked against the engagement's version. */
|
|
24839
24872
|
cvssVector: external_exports.string().max(200).optional(),
|
|
24873
|
+
/** Accepted for compatibility and ignored: the score is always derived from `cvssVector`. */
|
|
24840
24874
|
cvssScore: external_exports.number().min(0).max(10).optional(),
|
|
24841
24875
|
owaspCategory: external_exports.string().max(200).optional(),
|
|
24842
24876
|
proofOfConcept: external_exports.string().max(5e4).optional(),
|
|
@@ -24860,7 +24894,9 @@ var UpdatePentestFindingRequestSchema = external_exports.object({
|
|
|
24860
24894
|
category: external_exports.string().min(1).max(200).optional(),
|
|
24861
24895
|
status: PentestFindingStatus.optional(),
|
|
24862
24896
|
cweIds: external_exports.array(external_exports.string()).optional(),
|
|
24897
|
+
/** `CVSS:3.1/…` or `CVSS:4.0/…` (scored server-side, version-checked), or null to clear both columns. */
|
|
24863
24898
|
cvssVector: external_exports.string().max(200).nullable().optional(),
|
|
24899
|
+
/** Accepted for compatibility and ignored: the score is always derived from `cvssVector`. */
|
|
24864
24900
|
cvssScore: external_exports.number().min(0).max(10).nullable().optional(),
|
|
24865
24901
|
owaspCategory: external_exports.string().max(200).nullable().optional(),
|
|
24866
24902
|
proofOfConcept: external_exports.string().max(5e4).nullable().optional(),
|
|
@@ -25268,6 +25304,12 @@ var PentestResponseSchema = external_exports.object({
|
|
|
25268
25304
|
perHypothesisBudgetCapUsd: external_exports.number(),
|
|
25269
25305
|
startedAt: external_exports.coerce.date().nullable(),
|
|
25270
25306
|
endedAt: external_exports.coerce.date().nullable(),
|
|
25307
|
+
/**
|
|
25308
|
+
* Set while staff have curtained the engagement from every customer
|
|
25309
|
+
* principal; null = the customer can open it. `.optional()` for the same
|
|
25310
|
+
* rolling-deploy reason as `applications` above.
|
|
25311
|
+
*/
|
|
25312
|
+
hiddenFromCustomerAt: external_exports.coerce.date().nullable().optional(),
|
|
25271
25313
|
createdById: external_exports.uuid(),
|
|
25272
25314
|
createdByName: external_exports.string(),
|
|
25273
25315
|
createdAt: external_exports.coerce.date(),
|
|
@@ -25699,6 +25741,27 @@ var PasskeySecretSchema = external_exports.object({
|
|
|
25699
25741
|
/** largeBlob extension payload, base64. */
|
|
25700
25742
|
largeBlob: external_exports.string().max(8192).optional()
|
|
25701
25743
|
}).strict();
|
|
25744
|
+
var CredentialPasskeySummarySchema = external_exports.object({
|
|
25745
|
+
credentialPasskeyId: external_exports.uuid(),
|
|
25746
|
+
/** Relying party id: the hostname the passkey answers for. */
|
|
25747
|
+
rpId: external_exports.string().min(1).max(253),
|
|
25748
|
+
/**
|
|
25749
|
+
* The user name the site set at creation, read from the envelope. Null when the
|
|
25750
|
+
* site set none, or when `unreadable` is true.
|
|
25751
|
+
*/
|
|
25752
|
+
userName: external_exports.string().max(320).nullable(),
|
|
25753
|
+
/**
|
|
25754
|
+
* The envelope decrypted but did not parse as a passkey document: a permanent
|
|
25755
|
+
* state, and the agent skips this row too. A decrypt failure (KMS, a bad auth tag)
|
|
25756
|
+
* is not this flag; it fails the read, as every other envelope read does.
|
|
25757
|
+
*/
|
|
25758
|
+
unreadable: external_exports.boolean(),
|
|
25759
|
+
// Coerced, not `z.date()`: the web parses this out of the wire body with
|
|
25760
|
+
// safeParse, where the value is a string.
|
|
25761
|
+
createdAt: external_exports.coerce.date(),
|
|
25762
|
+
/** Moves only when the signature counter moved, the one observable sign of a use. */
|
|
25763
|
+
lastAssertedAt: external_exports.coerce.date().nullable()
|
|
25764
|
+
}).strict();
|
|
25702
25765
|
var CredentialMfaKindValues = ["TOTP"];
|
|
25703
25766
|
var CredentialMfaKind = external_exports.enum(CredentialMfaKindValues);
|
|
25704
25767
|
var MFA_CAPABLE_CREDENTIAL_TYPES = ["USERNAME_PASSWORD", "MAGIC_AUTH"];
|
|
@@ -26252,12 +26315,14 @@ var RecordValidationResultRequestSchema = external_exports.object({
|
|
|
26252
26315
|
*/
|
|
26253
26316
|
owaspCategory: external_exports.string().min(1).max(120).nullable().optional(),
|
|
26254
26317
|
/**
|
|
26255
|
-
* Agent-assigned CVSS
|
|
26256
|
-
* (PROVEN-only). The api derives and persists the
|
|
26257
|
-
* score AND the finding severity (from the score's
|
|
26258
|
-
* unparseable explicit vector is rejected (400
|
|
26259
|
-
*
|
|
26260
|
-
*
|
|
26318
|
+
* Agent-assigned CVSS base vector (`CVSS:3.1/…` or `CVSS:4.0/…`) for the
|
|
26319
|
+
* demonstrated impact (PROVEN-only). The api derives and persists the
|
|
26320
|
+
* canonical vector + base score AND the finding severity (from the score's
|
|
26321
|
+
* band) server-side; an unparseable explicit vector is rejected (400
|
|
26322
|
+
* INVALID_CVSS_VECTOR), and so is a vector of the other version than the
|
|
26323
|
+
* engagement's stored vectors (400 CVSS_VERSION_MISMATCH). The score is
|
|
26324
|
+
* NEVER client-supplied. Absent → both cvss columns null and severity falls
|
|
26325
|
+
* back to the agent's `severity` field (informational findings carry no vector).
|
|
26261
26326
|
*/
|
|
26262
26327
|
cvssVector: external_exports.string().min(1).max(120).nullable().optional(),
|
|
26263
26328
|
/**
|
|
@@ -27839,6 +27904,7 @@ var agentSessionInternalContract = c4.router({
|
|
|
27839
27904
|
body: OperatorApplyFindingCvssRequestSchema,
|
|
27840
27905
|
responses: {
|
|
27841
27906
|
200: OperatorFindingReadSchema,
|
|
27907
|
+
// INVALID_CVSS_VECTOR or CVSS_VERSION_MISMATCH (see the staff applyFindingCvss route).
|
|
27842
27908
|
400: ErrorResponseSchema,
|
|
27843
27909
|
401: ErrorResponseSchema,
|
|
27844
27910
|
404: ErrorResponseSchema,
|
|
@@ -29865,11 +29931,12 @@ var CodeSizeLanguageTallySchema = external_exports.object({
|
|
|
29865
29931
|
files: external_exports.number().int().nonnegative(),
|
|
29866
29932
|
code: external_exports.number().int().nonnegative()
|
|
29867
29933
|
});
|
|
29868
|
-
var CodeSizePlatformValues = ["GITHUB", "BITBUCKET"];
|
|
29934
|
+
var CodeSizePlatformValues = ["GITHUB", "BITBUCKET", "UPLOAD"];
|
|
29869
29935
|
var CodeSizeRepositorySchema = external_exports.object({
|
|
29870
29936
|
repositoryId: external_exports.uuid(),
|
|
29871
29937
|
repositoryUrl: external_exports.url(),
|
|
29872
29938
|
platform: external_exports.enum(CodeSizePlatformValues),
|
|
29939
|
+
displayName: external_exports.string(),
|
|
29873
29940
|
/** Billable SLOC of the current resolved branch. null = not measured yet. */
|
|
29874
29941
|
billableSloc: external_exports.number().int().nonnegative().nullable()
|
|
29875
29942
|
});
|
|
@@ -29881,7 +29948,8 @@ var CodeSizeBucketsSchema = external_exports.object({
|
|
|
29881
29948
|
vendored_generated: CodeSizeBucketTallySchema
|
|
29882
29949
|
});
|
|
29883
29950
|
var OrganizationCodeSizeResponseSchema = external_exports.object({
|
|
29884
|
-
/** All CONNECTED GitHub/Bitbucket repos
|
|
29951
|
+
/** All CONNECTED GitHub/Bitbucket repos plus READY source uploads.
|
|
29952
|
+
* Server-sorted: measured first by
|
|
29885
29953
|
* billableSloc desc (ties repositoryUrl asc), then unmeasured by
|
|
29886
29954
|
* repositoryUrl asc — clients render as-is (spec-pr-2 §3.3). */
|
|
29887
29955
|
repositories: external_exports.array(CodeSizeRepositorySchema),
|
|
@@ -29902,7 +29970,9 @@ var StaffCodeSizeRepositorySchema = external_exports.object({
|
|
|
29902
29970
|
repositoryId: external_exports.uuid(),
|
|
29903
29971
|
repositoryUrl: external_exports.url(),
|
|
29904
29972
|
platform: external_exports.enum(CodeSizePlatformValues),
|
|
29905
|
-
|
|
29973
|
+
displayName: external_exports.string(),
|
|
29974
|
+
/** Resolved branch the measurement is keyed on (scanningBranch ?? default;
|
|
29975
|
+
* `snapshot` for uploads). */
|
|
29906
29976
|
branch: external_exports.string(),
|
|
29907
29977
|
/** null = no successful measurement for the current resolved branch. */
|
|
29908
29978
|
billableSloc: external_exports.number().int().nonnegative().nullable(),
|
|
@@ -29920,7 +29990,8 @@ var StaffOrganizationCodeSizeResponseSchema = external_exports.object({
|
|
|
29920
29990
|
totals: external_exports.object({
|
|
29921
29991
|
billableSloc: external_exports.number().int().nonnegative(),
|
|
29922
29992
|
rawLines: external_exports.number().int().nonnegative(),
|
|
29923
|
-
/** Connected GitHub/Bitbucket repos
|
|
29993
|
+
/** Connected GitHub/Bitbucket repos plus READY source uploads: the
|
|
29994
|
+
* re-measure `all` scope size. */
|
|
29924
29995
|
repositoryCount: external_exports.number().int().nonnegative(),
|
|
29925
29996
|
/** Repos with a measurement for their current resolved branch. */
|
|
29926
29997
|
measuredCount: external_exports.number().int().nonnegative(),
|
|
@@ -30211,6 +30282,9 @@ var CredentialIdParamsSchema = OrgIdParamsSchema.extend({
|
|
|
30211
30282
|
environmentId: external_exports.uuid(),
|
|
30212
30283
|
credentialId: external_exports.uuid()
|
|
30213
30284
|
});
|
|
30285
|
+
var CredentialPasskeyIdParamsSchema = CredentialIdParamsSchema.extend({
|
|
30286
|
+
credentialPasskeyId: external_exports.uuid()
|
|
30287
|
+
});
|
|
30214
30288
|
var EnvironmentCredentialsParamsSchema = OrgIdParamsSchema.extend({
|
|
30215
30289
|
environmentId: external_exports.uuid()
|
|
30216
30290
|
});
|
|
@@ -30369,7 +30443,12 @@ var CredentialResponseSchema = external_exports.object({
|
|
|
30369
30443
|
* parameters only — the seed lives in its own KMS envelope on the row and
|
|
30370
30444
|
* has no reveal endpoint at all (see the reveal union below).
|
|
30371
30445
|
*/
|
|
30372
|
-
mfa: CredentialMfaSummarySchema.nullable()
|
|
30446
|
+
mfa: CredentialMfaSummarySchema.nullable(),
|
|
30447
|
+
/**
|
|
30448
|
+
* Passkeys an agent created on this account, oldest first. Always present; empty
|
|
30449
|
+
* for every credential type that cannot hold one. Summaries only.
|
|
30450
|
+
*/
|
|
30451
|
+
passkeys: external_exports.array(CredentialPasskeySummarySchema)
|
|
30373
30452
|
}).strict();
|
|
30374
30453
|
var ListCredentialsResponseSchema = external_exports.array(CredentialResponseSchema);
|
|
30375
30454
|
var UpdateCredentialRequestSchema = external_exports.object({
|
|
@@ -30592,6 +30671,27 @@ var credentialContract = c13.router({
|
|
|
30592
30671
|
500: ErrorResponseSchema
|
|
30593
30672
|
}
|
|
30594
30673
|
},
|
|
30674
|
+
// Remove ONE passkey Trace holds for the account. Not idempotent: the id names a
|
|
30675
|
+
// row, and a second call 404s like deleteCredential. Allowed mid-engagement like
|
|
30676
|
+
// clearCredentialMfa, and for the same reason: the customer decides what Trace
|
|
30677
|
+
// holds on their account. A session already armed keeps the key in Chrome's
|
|
30678
|
+
// memory until that browser closes, but cannot recreate the row (a counter tick
|
|
30679
|
+
// carries no key, a rejected tick is never re-sent, and a fresh arm reads the
|
|
30680
|
+
// store), so the removal sticks. Trace's copy only: the site keeps its own
|
|
30681
|
+
// registration of the public key until the customer removes it there.
|
|
30682
|
+
deleteCredentialPasskey: {
|
|
30683
|
+
method: "DELETE",
|
|
30684
|
+
path: "/organizations/:organizationId/environments/:environmentId/credentials/:credentialId/passkeys/:credentialPasskeyId",
|
|
30685
|
+
pathParams: CredentialPasskeyIdParamsSchema,
|
|
30686
|
+
responses: {
|
|
30687
|
+
// The refreshed credential, so the dashboard's row state comes from the api.
|
|
30688
|
+
200: CredentialResponseSchema,
|
|
30689
|
+
401: ErrorResponseSchema,
|
|
30690
|
+
403: ErrorResponseSchema,
|
|
30691
|
+
404: ErrorResponseSchema,
|
|
30692
|
+
500: ErrorResponseSchema
|
|
30693
|
+
}
|
|
30694
|
+
},
|
|
30595
30695
|
deleteCredential: {
|
|
30596
30696
|
method: "DELETE",
|
|
30597
30697
|
path: "/organizations/:organizationId/environments/:environmentId/credentials/:credentialId",
|
|
@@ -31082,6 +31182,11 @@ var EngagementCardsSchema = external_exports.object({
|
|
|
31082
31182
|
statusChangesViaPrComment: external_exports.number().int(),
|
|
31083
31183
|
botCommentsPosted: external_exports.number().int(),
|
|
31084
31184
|
structuralReplies: external_exports.number().int(),
|
|
31185
|
+
/**
|
|
31186
|
+
* Always 0. Its only source is the `issue_comment` webhook, which GitHub
|
|
31187
|
+
* does not deliver to any Trace app (GITHUB_APP_PERMISSIONS.md §Issue
|
|
31188
|
+
* comment). Read a reply figure as `structuralReplies` alone.
|
|
31189
|
+
*/
|
|
31085
31190
|
heuristicReplies: external_exports.number().int(),
|
|
31086
31191
|
/** structuralReplies / max(1, botCommentsPosted), clamped to [0, 1]. */
|
|
31087
31192
|
replyRate: external_exports.number().min(0).max(1)
|
|
@@ -34759,6 +34864,7 @@ var pentestFindingContract = c36.router({
|
|
|
34759
34864
|
body: CreatePentestFindingRequestSchema,
|
|
34760
34865
|
responses: {
|
|
34761
34866
|
201: PentestFindingResponseSchema,
|
|
34867
|
+
// INVALID_CVSS_VECTOR / CVSS_VERSION_MISMATCH when a cvssVector is sent, or schema validation.
|
|
34762
34868
|
400: ErrorResponseSchema,
|
|
34763
34869
|
401: ErrorResponseSchema,
|
|
34764
34870
|
403: ErrorResponseSchema,
|
|
@@ -34934,6 +35040,7 @@ var pentestFindingContract = c36.router({
|
|
|
34934
35040
|
body: UpdatePentestFindingRequestSchema,
|
|
34935
35041
|
responses: {
|
|
34936
35042
|
200: PentestFindingResponseSchema,
|
|
35043
|
+
// INVALID_CVSS_VECTOR / CVSS_VERSION_MISMATCH when a cvssVector is sent, or schema validation.
|
|
34937
35044
|
400: ErrorResponseSchema,
|
|
34938
35045
|
401: ErrorResponseSchema,
|
|
34939
35046
|
403: ErrorResponseSchema,
|
|
@@ -34980,7 +35087,8 @@ var pentestFindingContract = c36.router({
|
|
|
34980
35087
|
body: ApplyFindingCvssRequestSchema,
|
|
34981
35088
|
responses: {
|
|
34982
35089
|
200: PentestFindingResponseSchema,
|
|
34983
|
-
// INVALID_CVSS_VECTOR (unparseable vector)
|
|
35090
|
+
// INVALID_CVSS_VECTOR (unparseable vector), CVSS_VERSION_MISMATCH (the
|
|
35091
|
+
// engagement's stored vectors carry the other CVSS version), or schema validation.
|
|
34984
35092
|
400: ErrorResponseSchema,
|
|
34985
35093
|
401: ErrorResponseSchema,
|
|
34986
35094
|
403: ErrorResponseSchema,
|
|
@@ -36198,6 +36306,25 @@ var repositoryContract = c42.router({
|
|
|
36198
36306
|
500: ErrorResponseSchema
|
|
36199
36307
|
}
|
|
36200
36308
|
},
|
|
36309
|
+
/**
|
|
36310
|
+
* Include or exclude repositories from scanning. Excluded repos stop
|
|
36311
|
+
* receiving scans of every trigger type until included again; the flag is
|
|
36312
|
+
* customer-owned and survives provider re-syncs.
|
|
36313
|
+
*/
|
|
36314
|
+
setScanningInclusion: {
|
|
36315
|
+
method: "PUT",
|
|
36316
|
+
path: "/organizations/:organizationId/repositories/scanning-inclusion",
|
|
36317
|
+
pathParams: OrgIdParamsSchema,
|
|
36318
|
+
body: SetScanningInclusionRequestSchema,
|
|
36319
|
+
responses: {
|
|
36320
|
+
200: SetScanningInclusionResponseSchema,
|
|
36321
|
+
400: ErrorResponseSchema,
|
|
36322
|
+
401: ErrorResponseSchema,
|
|
36323
|
+
403: ErrorResponseSchema,
|
|
36324
|
+
404: ErrorResponseSchema,
|
|
36325
|
+
500: ErrorResponseSchema
|
|
36326
|
+
}
|
|
36327
|
+
},
|
|
36201
36328
|
getScanningBranchPreference: {
|
|
36202
36329
|
method: "GET",
|
|
36203
36330
|
path: "/organizations/:organizationId/repositories/scanning-branch/preference",
|
|
@@ -36521,11 +36648,17 @@ var StaffOrganizationPentestSummarySchema = external_exports.object({
|
|
|
36521
36648
|
findingCount: external_exports.int().nonnegative(),
|
|
36522
36649
|
startedAt: external_exports.iso.datetime().nullable(),
|
|
36523
36650
|
endedAt: external_exports.iso.datetime().nullable(),
|
|
36524
|
-
createdAt: external_exports.iso.datetime()
|
|
36651
|
+
createdAt: external_exports.iso.datetime(),
|
|
36652
|
+
/** Set while the engagement is curtained from every customer principal; null = visible. */
|
|
36653
|
+
hiddenFromCustomerAt: external_exports.iso.datetime().nullable()
|
|
36525
36654
|
}).strict();
|
|
36526
36655
|
var ListStaffOrganizationPentestsResponseSchema = external_exports.array(
|
|
36527
36656
|
StaffOrganizationPentestSummarySchema
|
|
36528
36657
|
);
|
|
36658
|
+
var SetStaffOrganizationPentestCustomerVisibilityRequestSchema = external_exports.object({
|
|
36659
|
+
/** false hides the engagement from the customer entirely; true shows it again. */
|
|
36660
|
+
visible: external_exports.boolean()
|
|
36661
|
+
}).strict();
|
|
36529
36662
|
|
|
36530
36663
|
// ../../packages/api-contracts/src/contracts/v1/staff-pentests.ts
|
|
36531
36664
|
var c45 = initContract();
|
|
@@ -36555,6 +36688,21 @@ var staffPentestsContract = c45.router({
|
|
|
36555
36688
|
500: ErrorResponseSchema
|
|
36556
36689
|
},
|
|
36557
36690
|
summary: "Soft-delete a pentest from the admin console (Trace staff only)"
|
|
36691
|
+
},
|
|
36692
|
+
setStaffOrganizationPentestCustomerVisibility: {
|
|
36693
|
+
method: "POST",
|
|
36694
|
+
path: "/organizations/:organizationId/staff/pentests/:pentestId/customer-visibility",
|
|
36695
|
+
pathParams: StaffOrganizationPentestParamsSchema,
|
|
36696
|
+
body: SetStaffOrganizationPentestCustomerVisibilityRequestSchema,
|
|
36697
|
+
responses: {
|
|
36698
|
+
200: StaffOrganizationPentestSummarySchema,
|
|
36699
|
+
401: ErrorResponseSchema,
|
|
36700
|
+
403: ErrorResponseSchema,
|
|
36701
|
+
404: ErrorResponseSchema,
|
|
36702
|
+
409: ErrorResponseSchema,
|
|
36703
|
+
500: ErrorResponseSchema
|
|
36704
|
+
},
|
|
36705
|
+
summary: "Hide a pentest from the customer or show it again (Trace staff only)"
|
|
36558
36706
|
}
|
|
36559
36707
|
});
|
|
36560
36708
|
|
|
@@ -37079,7 +37227,10 @@ var VulnerabilityLatestFindingSchema = external_exports.object({
|
|
|
37079
37227
|
classification: external_exports.enum(["vulnerability", "defense_in_depth", "context_dependent"]).nullable(),
|
|
37080
37228
|
prerequisiteAccess: external_exports.string().nullable(),
|
|
37081
37229
|
capabilityGain: external_exports.string().nullable(),
|
|
37082
|
-
technicalSeverity: VulnerabilitySeverity.nullable()
|
|
37230
|
+
technicalSeverity: VulnerabilitySeverity.nullable(),
|
|
37231
|
+
// Whether the scan found an entry point that reaches the code; null when
|
|
37232
|
+
// no scan assessed it (reads as reachable).
|
|
37233
|
+
reachability: external_exports.enum(["reachable", "unreachable"]).nullable()
|
|
37083
37234
|
});
|
|
37084
37235
|
var DependencyGraphSchema = external_exports.object({
|
|
37085
37236
|
vulnerablePackage: external_exports.string(),
|
|
@@ -37451,6 +37602,7 @@ var vulnerabilityContract = c50.router({
|
|
|
37451
37602
|
402: ErrorResponseSchema,
|
|
37452
37603
|
403: ErrorResponseSchema,
|
|
37453
37604
|
404: ErrorResponseSchema,
|
|
37605
|
+
409: ErrorResponseSchema,
|
|
37454
37606
|
500: ErrorResponseSchema
|
|
37455
37607
|
}
|
|
37456
37608
|
},
|
|
@@ -38234,7 +38386,7 @@ async function requestDeviceAuthorization(clientId) {
|
|
|
38234
38386
|
}
|
|
38235
38387
|
return await response.json();
|
|
38236
38388
|
}
|
|
38237
|
-
var sleep = (ms) => new Promise((
|
|
38389
|
+
var sleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
38238
38390
|
async function pollForToken(clientId, deviceCode, options = {}) {
|
|
38239
38391
|
const expiresIn = options.expiresIn ?? 300;
|
|
38240
38392
|
let interval = options.interval ?? 5;
|
|
@@ -38919,18 +39071,18 @@ var proto = Object.defineProperties(() => {
|
|
|
38919
39071
|
}
|
|
38920
39072
|
}
|
|
38921
39073
|
});
|
|
38922
|
-
var createStyler = (
|
|
39074
|
+
var createStyler = (open4, close, parent) => {
|
|
38923
39075
|
let openAll;
|
|
38924
39076
|
let closeAll;
|
|
38925
39077
|
if (parent === void 0) {
|
|
38926
|
-
openAll =
|
|
39078
|
+
openAll = open4;
|
|
38927
39079
|
closeAll = close;
|
|
38928
39080
|
} else {
|
|
38929
|
-
openAll = parent.openAll +
|
|
39081
|
+
openAll = parent.openAll + open4;
|
|
38930
39082
|
closeAll = close + parent.closeAll;
|
|
38931
39083
|
}
|
|
38932
39084
|
return {
|
|
38933
|
-
open:
|
|
39085
|
+
open: open4,
|
|
38934
39086
|
close,
|
|
38935
39087
|
openAll,
|
|
38936
39088
|
closeAll,
|
|
@@ -39513,6 +39665,9 @@ function formatStatus(status) {
|
|
|
39513
39665
|
function tsvSafe(value) {
|
|
39514
39666
|
return (value ?? "").replace(/[\t\r\n]/g, " ");
|
|
39515
39667
|
}
|
|
39668
|
+
function formulaSafe(value) {
|
|
39669
|
+
return /^[=+\-@]/.test(value) ? `'${value}` : value;
|
|
39670
|
+
}
|
|
39516
39671
|
|
|
39517
39672
|
// src/pager.ts
|
|
39518
39673
|
function isInteractiveEnvironment() {
|
|
@@ -39627,6 +39782,21 @@ function extractRepoName(input) {
|
|
|
39627
39782
|
}
|
|
39628
39783
|
return input;
|
|
39629
39784
|
}
|
|
39785
|
+
function normalizeRepoUrlForMatch(url2) {
|
|
39786
|
+
let normalized = url2.trim().toLowerCase();
|
|
39787
|
+
const scp = normalized.match(/^[a-z0-9._-]+@([^:/]+):(.+)$/);
|
|
39788
|
+
if (scp) {
|
|
39789
|
+
normalized = `${scp[1]}/${scp[2]}`;
|
|
39790
|
+
} else {
|
|
39791
|
+
normalized = normalized.replace(/^[a-z][a-z0-9+.-]*:\/\//, "").replace(/^[a-z0-9._-]+@/, "");
|
|
39792
|
+
}
|
|
39793
|
+
let previous;
|
|
39794
|
+
do {
|
|
39795
|
+
previous = normalized;
|
|
39796
|
+
normalized = normalized.replace(/\/+$/, "").replace(/\.git$/, "");
|
|
39797
|
+
} while (normalized !== previous);
|
|
39798
|
+
return normalized;
|
|
39799
|
+
}
|
|
39630
39800
|
var BRANCH_NAME_PATTERN = /^[a-zA-Z0-9._/+-]+$/;
|
|
39631
39801
|
var TRACE_GIT_ASKPASS_TOKEN_ENV = "TRACE_GIT_ASKPASS_TOKEN";
|
|
39632
39802
|
var DEFAULT_LIMIT = 30;
|
|
@@ -39634,6 +39804,8 @@ var DEFAULT_PAGE = 1;
|
|
|
39634
39804
|
var MAX_LIMIT = 100;
|
|
39635
39805
|
var REPOSITORY_PLATFORMS = ["GITHUB", "GITLAB", "BITBUCKET"];
|
|
39636
39806
|
var REPOSITORY_STATUSES = ["CONNECTED", "DISCONNECTED"];
|
|
39807
|
+
var SCANNING_INCLUSIONS = ["included", "excluded"];
|
|
39808
|
+
var SCANNING_INCLUSION_BATCH_SIZE = 500;
|
|
39637
39809
|
function validateBranchName(branch) {
|
|
39638
39810
|
if (branch.startsWith("-")) {
|
|
39639
39811
|
throw new Error(`Invalid branch name: '${branch}' \u2014 branch names cannot start with a dash`);
|
|
@@ -39687,6 +39859,18 @@ function normalizeStatus(value, source) {
|
|
|
39687
39859
|
`${source} must be one of: ${REPOSITORY_STATUSES.join(", ")}. Received '${value}'.`
|
|
39688
39860
|
);
|
|
39689
39861
|
}
|
|
39862
|
+
function normalizeScanningInclusion(value, source) {
|
|
39863
|
+
const normalized = value.trim().toLowerCase();
|
|
39864
|
+
if (SCANNING_INCLUSIONS.includes(normalized)) {
|
|
39865
|
+
return normalized;
|
|
39866
|
+
}
|
|
39867
|
+
throw new Error(
|
|
39868
|
+
`${source} must be one of: ${SCANNING_INCLUSIONS.join(", ")}. Received '${value}'.`
|
|
39869
|
+
);
|
|
39870
|
+
}
|
|
39871
|
+
function scanningInclusionOf(repo) {
|
|
39872
|
+
return repo.includedInScanning ? "included" : "excluded";
|
|
39873
|
+
}
|
|
39690
39874
|
function normalizeFilterField(rawField) {
|
|
39691
39875
|
const field = rawField.trim().toLowerCase();
|
|
39692
39876
|
switch (field) {
|
|
@@ -39711,9 +39895,14 @@ function normalizeFilterField(rawField) {
|
|
|
39711
39895
|
case "primarylanguage":
|
|
39712
39896
|
case "primary_language":
|
|
39713
39897
|
return "language";
|
|
39898
|
+
case "scanning":
|
|
39899
|
+
case "inclusion":
|
|
39900
|
+
case "included_in_scanning":
|
|
39901
|
+
case "includedinscanning":
|
|
39902
|
+
return "scanning";
|
|
39714
39903
|
default:
|
|
39715
39904
|
throw new Error(
|
|
39716
|
-
`Unknown repository filter field '${rawField}'. Supported fields: platform, status, url, repo, branch, language.`
|
|
39905
|
+
`Unknown repository filter field '${rawField}'. Supported fields: platform, status, url, repo, branch, language, scanning.`
|
|
39717
39906
|
);
|
|
39718
39907
|
}
|
|
39719
39908
|
}
|
|
@@ -39757,6 +39946,9 @@ function parseRepositoryFilterExpression(expression) {
|
|
|
39757
39946
|
if (field === "status") {
|
|
39758
39947
|
normalizeStatus(value, "--filter");
|
|
39759
39948
|
}
|
|
39949
|
+
if (field === "scanning") {
|
|
39950
|
+
normalizeScanningInclusion(value, "--filter");
|
|
39951
|
+
}
|
|
39760
39952
|
return { field, operator, value };
|
|
39761
39953
|
}
|
|
39762
39954
|
function doesValueMatchFilter(candidate, filterValue, operator, exactMatchOnly) {
|
|
@@ -39764,7 +39956,7 @@ function doesValueMatchFilter(candidate, filterValue, operator, exactMatchOnly)
|
|
|
39764
39956
|
const normalizedFilter = normalizeForMatching(filterValue);
|
|
39765
39957
|
if (exactMatchOnly || operator === "exact" || operator === "not_exact") {
|
|
39766
39958
|
const matches2 = normalizedCandidate === normalizedFilter;
|
|
39767
|
-
return operator === "not_exact" ? !matches2 : matches2;
|
|
39959
|
+
return operator === "not_exact" || operator === "not_contains" ? !matches2 : matches2;
|
|
39768
39960
|
}
|
|
39769
39961
|
const matches = normalizedCandidate.includes(normalizedFilter);
|
|
39770
39962
|
return operator === "not_contains" ? !matches : matches;
|
|
@@ -39800,6 +39992,13 @@ function matchParsedFilter(repo, parsedFilter) {
|
|
|
39800
39992
|
const language = repo.primaryLanguage ?? "";
|
|
39801
39993
|
return doesValueMatchFilter(language, parsedFilter.value, parsedFilter.operator, false);
|
|
39802
39994
|
}
|
|
39995
|
+
case "scanning":
|
|
39996
|
+
return doesValueMatchFilter(
|
|
39997
|
+
scanningInclusionOf(repo),
|
|
39998
|
+
parsedFilter.value,
|
|
39999
|
+
parsedFilter.operator,
|
|
40000
|
+
true
|
|
40001
|
+
);
|
|
39803
40002
|
}
|
|
39804
40003
|
}
|
|
39805
40004
|
function filterRepositories(repositories, options) {
|
|
@@ -39811,6 +40010,7 @@ function filterRepositories(repositories, options) {
|
|
|
39811
40010
|
const normalizedQuery = options.query ? normalizeForMatching(options.query) : void 0;
|
|
39812
40011
|
const normalizedDefaultBranch = options.githubDefaultBranch ? normalizeForMatching(options.githubDefaultBranch) : void 0;
|
|
39813
40012
|
const normalizedLanguage = options.language ? normalizeForMatching(options.language) : void 0;
|
|
40013
|
+
const normalizedScanning = options.scanning ? normalizeScanningInclusion(options.scanning, "--scanning") : void 0;
|
|
39814
40014
|
return repositories.filter((repo) => {
|
|
39815
40015
|
if (normalizedPlatform && repo.platform.toUpperCase() !== normalizedPlatform) {
|
|
39816
40016
|
return false;
|
|
@@ -39818,6 +40018,9 @@ function filterRepositories(repositories, options) {
|
|
|
39818
40018
|
if (normalizedStatus && repo.status.toUpperCase() !== normalizedStatus) {
|
|
39819
40019
|
return false;
|
|
39820
40020
|
}
|
|
40021
|
+
if (normalizedScanning && scanningInclusionOf(repo) !== normalizedScanning) {
|
|
40022
|
+
return false;
|
|
40023
|
+
}
|
|
39821
40024
|
if (normalizedDefaultBranch && !normalizeForMatching(repo.githubDefaultBranch).includes(normalizedDefaultBranch)) {
|
|
39822
40025
|
return false;
|
|
39823
40026
|
}
|
|
@@ -39890,7 +40093,9 @@ function paginateResults(items, options) {
|
|
|
39890
40093
|
};
|
|
39891
40094
|
}
|
|
39892
40095
|
function printRepositoriesTsv(repositories) {
|
|
39893
|
-
console.log(
|
|
40096
|
+
console.log(
|
|
40097
|
+
"repositoryUrl platform status githubDefaultBranch scanningBranch language scanning"
|
|
40098
|
+
);
|
|
39894
40099
|
for (const repo of repositories) {
|
|
39895
40100
|
console.log(
|
|
39896
40101
|
[
|
|
@@ -39899,18 +40104,20 @@ function printRepositoriesTsv(repositories) {
|
|
|
39899
40104
|
tsvSafe(repo.status),
|
|
39900
40105
|
tsvSafe(repo.githubDefaultBranch),
|
|
39901
40106
|
tsvSafe(repo.scanningBranch ?? ""),
|
|
39902
|
-
tsvSafe(repo.primaryLanguage)
|
|
40107
|
+
tsvSafe(repo.primaryLanguage),
|
|
40108
|
+
scanningInclusionOf(repo)
|
|
39903
40109
|
].join(" ")
|
|
39904
40110
|
);
|
|
39905
40111
|
}
|
|
39906
40112
|
}
|
|
39907
40113
|
function buildRepositoriesTable(repositories) {
|
|
39908
|
-
const headers = ["Repository URL", "Platform", "Status"];
|
|
40114
|
+
const headers = ["Repository URL", "Platform", "Status", "Scanning"];
|
|
39909
40115
|
const rows = repositories.map(
|
|
39910
40116
|
(repo) => [
|
|
39911
40117
|
stripAnsi(String(repo.repositoryUrl)),
|
|
39912
40118
|
stripAnsi(String(repo.platform)).toUpperCase(),
|
|
39913
|
-
repo.status === "CONNECTED" ? "connected" : "disconnected"
|
|
40119
|
+
repo.status === "CONNECTED" ? "connected" : "disconnected",
|
|
40120
|
+
scanningInclusionOf(repo)
|
|
39914
40121
|
]
|
|
39915
40122
|
);
|
|
39916
40123
|
const colWidths = headers.map(
|
|
@@ -40039,13 +40246,27 @@ async function maybeRefreshCloneTokenSessionForOrganization(organizationId, list
|
|
|
40039
40246
|
}
|
|
40040
40247
|
return refreshForOrganization(workosOrganizationId);
|
|
40041
40248
|
}
|
|
40249
|
+
function resolveScanningFlag(options) {
|
|
40250
|
+
if (options.included && options.excluded) {
|
|
40251
|
+
throw new Error("Pass only one of --included / --excluded.");
|
|
40252
|
+
}
|
|
40253
|
+
if (options.included) {
|
|
40254
|
+
return "included";
|
|
40255
|
+
}
|
|
40256
|
+
if (options.excluded) {
|
|
40257
|
+
return "excluded";
|
|
40258
|
+
}
|
|
40259
|
+
return void 0;
|
|
40260
|
+
}
|
|
40042
40261
|
function toRepositoryListFilterOptions(options) {
|
|
40262
|
+
const scanning = resolveScanningFlag(options);
|
|
40043
40263
|
return {
|
|
40044
40264
|
...options.platform ? { platform: options.platform } : {},
|
|
40045
40265
|
...options.status ? { status: options.status } : {},
|
|
40046
40266
|
...options.query ? { query: options.query } : {},
|
|
40047
40267
|
...options.defaultBranch ? { githubDefaultBranch: options.defaultBranch } : {},
|
|
40048
40268
|
...options.language ? { language: options.language } : {},
|
|
40269
|
+
...scanning ? { scanning } : {},
|
|
40049
40270
|
...options.filter.length > 0 ? { filters: options.filter } : {}
|
|
40050
40271
|
};
|
|
40051
40272
|
}
|
|
@@ -40058,9 +40279,9 @@ function addRepositoryFilterOptions(command) {
|
|
|
40058
40279
|
"--status <status>",
|
|
40059
40280
|
"Filter by status (CONNECTED, DISCONNECTED)",
|
|
40060
40281
|
(value) => value.toUpperCase()
|
|
40061
|
-
).option("--query <query>", "Case-insensitive search across URL, repo, branch, and language").option("--default-branch <branch>", "Filter by default branch (case-insensitive contains)").option("--language <language>", "Filter by primary language (case-insensitive contains)").option(
|
|
40282
|
+
).option("--query <query>", "Case-insensitive search across URL, repo, branch, and language").option("--default-branch <branch>", "Filter by default branch (case-insensitive contains)").option("--language <language>", "Filter by primary language (case-insensitive contains)").option("--included", "Only repositories included in scanning").option("--excluded", "Only repositories excluded from scanning").option(
|
|
40062
40283
|
"-f, --filter <expression>",
|
|
40063
|
-
"Advanced filter expression. Repeatable. Examples: platform=GITHUB, repo:trace, status!=DISCONNECTED",
|
|
40284
|
+
"Advanced filter expression. Repeatable. Examples: platform=GITHUB, repo:trace, status!=DISCONNECTED, scanning=excluded",
|
|
40064
40285
|
collectFilterExpression,
|
|
40065
40286
|
[]
|
|
40066
40287
|
).option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").option(
|
|
@@ -40088,6 +40309,14 @@ async function runRepositoryListCommand(options) {
|
|
|
40088
40309
|
}
|
|
40089
40310
|
return;
|
|
40090
40311
|
}
|
|
40312
|
+
let filterOptions;
|
|
40313
|
+
try {
|
|
40314
|
+
filterOptions = toRepositoryListFilterOptions(options);
|
|
40315
|
+
} catch (error48) {
|
|
40316
|
+
const message = error48 instanceof Error ? error48.message : "Unknown filter parsing error.";
|
|
40317
|
+
console.error(`Invalid repository filter: ${message}`);
|
|
40318
|
+
process.exit(1);
|
|
40319
|
+
}
|
|
40091
40320
|
const organizationId = await resolveOrganizationId();
|
|
40092
40321
|
if (options.dependency) {
|
|
40093
40322
|
await runDependencyFilter(organizationId, {
|
|
@@ -40111,14 +40340,14 @@ async function runRepositoryListCommand(options) {
|
|
|
40111
40340
|
process.exit(1);
|
|
40112
40341
|
}
|
|
40113
40342
|
try {
|
|
40114
|
-
filteredRepositories = filterRepositories(result.body,
|
|
40343
|
+
filteredRepositories = filterRepositories(result.body, filterOptions);
|
|
40115
40344
|
} catch (error48) {
|
|
40116
40345
|
const message = error48 instanceof Error ? error48.message : "Unknown filter parsing error.";
|
|
40117
40346
|
console.error(`Invalid repository filter: ${message}`);
|
|
40118
40347
|
process.exit(1);
|
|
40119
40348
|
}
|
|
40120
40349
|
if (filteredRepositories.length === 0) {
|
|
40121
|
-
if (options.platform || options.status || options.query || options.defaultBranch || options.language || options.filter.length > 0) {
|
|
40350
|
+
if (options.platform || options.status || options.query || options.defaultBranch || options.language || options.included || options.excluded || options.filter.length > 0) {
|
|
40122
40351
|
console.log("No repositories found matching the provided filters.");
|
|
40123
40352
|
return;
|
|
40124
40353
|
}
|
|
@@ -40252,6 +40481,8 @@ Examples:
|
|
|
40252
40481
|
tracecli repo list --output json | jq '.[].repositoryUrl'
|
|
40253
40482
|
tracecli repo list --output tsv | cut -f1
|
|
40254
40483
|
tracecli repo list --filter "language:typescript" --limit 10
|
|
40484
|
+
tracecli repo list --excluded
|
|
40485
|
+
tracecli repo list --query payments --included --output json
|
|
40255
40486
|
tracecli repo list --dependency lodash@4.17.21
|
|
40256
40487
|
tracecli repo list --web`
|
|
40257
40488
|
).action(async (options) => {
|
|
@@ -40269,6 +40500,7 @@ Examples:
|
|
|
40269
40500
|
tracecli repo filter --page 2 --limit 30
|
|
40270
40501
|
tracecli repo filter --all
|
|
40271
40502
|
tracecli repo filter --language typescript --limit 10
|
|
40503
|
+
tracecli repo filter --filter "scanning=excluded"
|
|
40272
40504
|
tracecli repo filter --output json | jq '.[].repositoryUrl'
|
|
40273
40505
|
tracecli repo filter --output tsv | cut -f1
|
|
40274
40506
|
tracecli repo filter --web`
|
|
@@ -40631,12 +40863,158 @@ Examples:
|
|
|
40631
40863
|
await runSetScanningBranchCommand(repositoryUrl, options);
|
|
40632
40864
|
});
|
|
40633
40865
|
}
|
|
40866
|
+
function resolveRepositoryReferences(repositories, references) {
|
|
40867
|
+
const byId = /* @__PURE__ */ new Map();
|
|
40868
|
+
const byUrl = /* @__PURE__ */ new Map();
|
|
40869
|
+
for (const repo of repositories) {
|
|
40870
|
+
byId.set(repo.repositoryId.toLowerCase(), repo);
|
|
40871
|
+
byUrl.set(normalizeRepoUrlForMatch(repo.repositoryUrl), repo);
|
|
40872
|
+
}
|
|
40873
|
+
const matched = /* @__PURE__ */ new Map();
|
|
40874
|
+
const unresolved = [];
|
|
40875
|
+
for (const reference of references) {
|
|
40876
|
+
const trimmed = reference.trim();
|
|
40877
|
+
if (trimmed.length === 0) {
|
|
40878
|
+
continue;
|
|
40879
|
+
}
|
|
40880
|
+
const repo = byId.get(trimmed.toLowerCase()) ?? byUrl.get(normalizeRepoUrlForMatch(resolveRepoUrl(trimmed)));
|
|
40881
|
+
if (!repo) {
|
|
40882
|
+
unresolved.push(trimmed);
|
|
40883
|
+
continue;
|
|
40884
|
+
}
|
|
40885
|
+
if (!matched.has(repo.repositoryId)) {
|
|
40886
|
+
matched.set(repo.repositoryId, repo);
|
|
40887
|
+
}
|
|
40888
|
+
}
|
|
40889
|
+
return { matched: [...matched.values()], unresolved };
|
|
40890
|
+
}
|
|
40891
|
+
function scanningInclusionVerb(includedInScanning) {
|
|
40892
|
+
return includedInScanning ? { past: "Included", noun: "included in scanning" } : { past: "Excluded", noun: "excluded from scanning" };
|
|
40893
|
+
}
|
|
40894
|
+
async function runSetScanningInclusionCommand(references, includedInScanning, options) {
|
|
40895
|
+
const organizationId = await resolveOrganizationId();
|
|
40896
|
+
const listResult = await authenticatedRequest(
|
|
40897
|
+
(client) => client.repository.listRepositories({ params: { organizationId } })
|
|
40898
|
+
);
|
|
40899
|
+
if (listResult.status === 401) {
|
|
40900
|
+
console.error("Authentication failed. Run `tracecli auth login` to re-authenticate.");
|
|
40901
|
+
process.exit(1);
|
|
40902
|
+
}
|
|
40903
|
+
if (listResult.status !== 200) {
|
|
40904
|
+
console.error(`Failed to list repositories (status ${listResult.status}).`);
|
|
40905
|
+
process.exit(1);
|
|
40906
|
+
}
|
|
40907
|
+
const { matched, unresolved } = resolveRepositoryReferences(listResult.body, references);
|
|
40908
|
+
if (unresolved.length > 0) {
|
|
40909
|
+
console.error(
|
|
40910
|
+
colorError(
|
|
40911
|
+
`${unresolved.length === 1 ? "Repository" : "Repositories"} not found in this organization: ${unresolved.join(", ")}`
|
|
40912
|
+
)
|
|
40913
|
+
);
|
|
40914
|
+
console.error(
|
|
40915
|
+
"Pass a repository ID, URL, or owner/repo shorthand. Run `tracecli repo list` to see connected repositories."
|
|
40916
|
+
);
|
|
40917
|
+
process.exit(1);
|
|
40918
|
+
}
|
|
40919
|
+
if (matched.length === 0) {
|
|
40920
|
+
console.error(colorError("No repositories given."));
|
|
40921
|
+
process.exit(1);
|
|
40922
|
+
}
|
|
40923
|
+
const outcomes = matched.map((repo) => ({
|
|
40924
|
+
repositoryId: repo.repositoryId,
|
|
40925
|
+
repositoryUrl: repo.repositoryUrl,
|
|
40926
|
+
previouslyIncludedInScanning: repo.includedInScanning
|
|
40927
|
+
}));
|
|
40928
|
+
const ids = matched.map((repo) => repo.repositoryId);
|
|
40929
|
+
let updatedCount = 0;
|
|
40930
|
+
const notFoundRepositoryIds = [];
|
|
40931
|
+
const verb = scanningInclusionVerb(includedInScanning);
|
|
40932
|
+
for (let offset = 0; offset < ids.length; offset += SCANNING_INCLUSION_BATCH_SIZE) {
|
|
40933
|
+
const result = await authenticatedRequest(
|
|
40934
|
+
(client) => client.repository.setScanningInclusion({
|
|
40935
|
+
params: { organizationId },
|
|
40936
|
+
body: {
|
|
40937
|
+
repositoryIds: ids.slice(offset, offset + SCANNING_INCLUSION_BATCH_SIZE),
|
|
40938
|
+
includedInScanning
|
|
40939
|
+
}
|
|
40940
|
+
})
|
|
40941
|
+
);
|
|
40942
|
+
if (result.status === 200) {
|
|
40943
|
+
updatedCount += result.body.updatedCount;
|
|
40944
|
+
notFoundRepositoryIds.push(...result.body.notFoundRepositoryIds);
|
|
40945
|
+
continue;
|
|
40946
|
+
}
|
|
40947
|
+
const committedSuffix = offset > 0 ? ` ${updatedCount} of ${ids.length} repositories were already updated.` : "";
|
|
40948
|
+
if (result.status === 401) {
|
|
40949
|
+
console.error("Authentication failed. Run `tracecli auth login` to re-authenticate.");
|
|
40950
|
+
} else if (result.status === 403) {
|
|
40951
|
+
console.error(
|
|
40952
|
+
colorError(
|
|
40953
|
+
`You don't have permission to change repository scanning in this organization (analyst or admin role required).${committedSuffix}`
|
|
40954
|
+
)
|
|
40955
|
+
);
|
|
40956
|
+
} else {
|
|
40957
|
+
const message = extractErrorMessage(result.body);
|
|
40958
|
+
console.error(
|
|
40959
|
+
colorError(
|
|
40960
|
+
`Failed to update repository scanning (status ${result.status})${message ? `: ${message}` : "."}${committedSuffix}`
|
|
40961
|
+
)
|
|
40962
|
+
);
|
|
40963
|
+
}
|
|
40964
|
+
process.exit(1);
|
|
40965
|
+
}
|
|
40966
|
+
if (options.output === "json") {
|
|
40967
|
+
console.log(
|
|
40968
|
+
JSON.stringify(
|
|
40969
|
+
{ includedInScanning, updatedCount, notFoundRepositoryIds, repositories: outcomes },
|
|
40970
|
+
null,
|
|
40971
|
+
2
|
|
40972
|
+
)
|
|
40973
|
+
);
|
|
40974
|
+
return;
|
|
40975
|
+
}
|
|
40976
|
+
const notFound = new Set(notFoundRepositoryIds);
|
|
40977
|
+
for (const outcome of outcomes) {
|
|
40978
|
+
if (notFound.has(outcome.repositoryId)) {
|
|
40979
|
+
console.log(
|
|
40980
|
+
`${colorWarningPrefix("Skipped")} ${outcome.repositoryUrl}: no longer connected to this organization`
|
|
40981
|
+
);
|
|
40982
|
+
} else if (outcome.previouslyIncludedInScanning === includedInScanning) {
|
|
40983
|
+
console.log(`Already ${verb.noun}: ${outcome.repositoryUrl}`);
|
|
40984
|
+
} else {
|
|
40985
|
+
console.log(`${colorSuccess(verb.past)} ${outcome.repositoryUrl}`);
|
|
40986
|
+
}
|
|
40987
|
+
}
|
|
40988
|
+
const changedCount = outcomes.filter(
|
|
40989
|
+
(outcome) => !notFound.has(outcome.repositoryId) && outcome.previouslyIncludedInScanning !== includedInScanning
|
|
40990
|
+
).length;
|
|
40991
|
+
console.log(
|
|
40992
|
+
`${changedCount} of ${outcomes.length} ${outcomes.length === 1 ? "repository" : "repositories"} changed to ${verb.noun}.${includedInScanning ? "" : " Excluded repositories are skipped by every scan until you include them again."}`
|
|
40993
|
+
);
|
|
40994
|
+
}
|
|
40995
|
+
function createScanningInclusionCommand(includedInScanning) {
|
|
40996
|
+
const name = includedInScanning ? "include" : "exclude";
|
|
40997
|
+
const description = includedInScanning ? "Include repositories in scanning" : "Exclude repositories from scanning (no scans of any kind run until included again)";
|
|
40998
|
+
return new Command(name).description(description).argument("<repository...>", "Repository ID, URL, or owner/repo shorthand (repeatable)").option("-o, --output <format>", "Output format: table (default), json", "table").addHelpText(
|
|
40999
|
+
"after",
|
|
41000
|
+
`
|
|
41001
|
+
Examples:
|
|
41002
|
+
tracecli repo ${name} acme/api
|
|
41003
|
+
tracecli repo ${name} https://github.com/acme/api https://bitbucket.org/acme/worker
|
|
41004
|
+
tracecli repo ${name} 3f1c6c2e-9b7a-4a6f-8f1d-2d4e5a6b7c8d --output json
|
|
41005
|
+
tracecli repo list --query legacy --output json | jq -r '.[].repositoryUrl' | xargs tracecli repo ${name}`
|
|
41006
|
+
).action(async (repositories, options) => {
|
|
41007
|
+
await runSetScanningInclusionCommand(repositories, includedInScanning, options);
|
|
41008
|
+
});
|
|
41009
|
+
}
|
|
40634
41010
|
function createRepoCommand() {
|
|
40635
41011
|
const repo = new Command("repo").description("Manage repositories");
|
|
40636
41012
|
repo.addCommand(createListCommand());
|
|
40637
41013
|
repo.addCommand(createFilterCommand());
|
|
40638
41014
|
repo.addCommand(createCloneCommand());
|
|
40639
41015
|
repo.addCommand(createSetScanningBranchCommand());
|
|
41016
|
+
repo.addCommand(createScanningInclusionCommand(true));
|
|
41017
|
+
repo.addCommand(createScanningInclusionCommand(false));
|
|
40640
41018
|
return repo;
|
|
40641
41019
|
}
|
|
40642
41020
|
|
|
@@ -40745,21 +41123,6 @@ function validateCreateTypeUrl(type, url2) {
|
|
|
40745
41123
|
}
|
|
40746
41124
|
return null;
|
|
40747
41125
|
}
|
|
40748
|
-
function normalizeRepoUrlForMatch(url2) {
|
|
40749
|
-
let normalized = url2.trim().toLowerCase();
|
|
40750
|
-
const scp = normalized.match(/^[a-z0-9._-]+@([^:/]+):(.+)$/);
|
|
40751
|
-
if (scp) {
|
|
40752
|
-
normalized = `${scp[1]}/${scp[2]}`;
|
|
40753
|
-
} else {
|
|
40754
|
-
normalized = normalized.replace(/^[a-z][a-z0-9+.-]*:\/\//, "").replace(/^[a-z0-9._-]+@/, "");
|
|
40755
|
-
}
|
|
40756
|
-
let previous;
|
|
40757
|
-
do {
|
|
40758
|
-
previous = normalized;
|
|
40759
|
-
normalized = normalized.replace(/\/+$/, "").replace(/\.git$/, "");
|
|
40760
|
-
} while (normalized !== previous);
|
|
40761
|
-
return normalized;
|
|
40762
|
-
}
|
|
40763
41126
|
function buildCreateApplicationBody(input) {
|
|
40764
41127
|
return {
|
|
40765
41128
|
name: input.name,
|
|
@@ -43713,6 +44076,9 @@ function repoDisplayName(repositoryUrl) {
|
|
|
43713
44076
|
return repositoryUrl;
|
|
43714
44077
|
}
|
|
43715
44078
|
}
|
|
44079
|
+
function repoLabel(repo) {
|
|
44080
|
+
return sanitizeDisplay(repo.displayName ?? repoDisplayName(repo.repositoryUrl));
|
|
44081
|
+
}
|
|
43716
44082
|
function countedRows(totals) {
|
|
43717
44083
|
const b = totals.buckets;
|
|
43718
44084
|
return [
|
|
@@ -43769,7 +44135,7 @@ function renderCodeSizeTable(codeSize, options) {
|
|
|
43769
44135
|
lines.push(
|
|
43770
44136
|
...padValues(
|
|
43771
44137
|
shown.map((repo) => ({
|
|
43772
|
-
label:
|
|
44138
|
+
label: repoLabel(repo),
|
|
43773
44139
|
value: repo.billableSloc ?? 0
|
|
43774
44140
|
}))
|
|
43775
44141
|
)
|
|
@@ -43784,7 +44150,8 @@ function renderCodeSizeTsv(codeSize) {
|
|
|
43784
44150
|
return [
|
|
43785
44151
|
"repository billableSloc",
|
|
43786
44152
|
...codeSize.repositories.map(
|
|
43787
|
-
|
|
44153
|
+
// Upload names are customer-authored, and TSV lands in spreadsheets.
|
|
44154
|
+
(repo) => `${formulaSafe(repoLabel(repo))} ${repo.billableSloc === null ? "" : repo.billableSloc}`
|
|
43788
44155
|
)
|
|
43789
44156
|
];
|
|
43790
44157
|
}
|
|
@@ -46657,185 +47024,159 @@ async function runCreateFinding(options) {
|
|
|
46657
47024
|
});
|
|
46658
47025
|
}
|
|
46659
47026
|
function createCreateFindingCommand() {
|
|
46660
|
-
return new Command("create").description("Create a pentest finding from a contractor write-up (staff)").requiredOption("--pentest <id>", "Pentest display ID (e.g. PT-3) or UUID").option("--title <text>", "Finding title").option("--description <text>", "Finding description").option("--description-file <path>", "Read description from a file").option("--severity <severity>", `Severity (${SEVERITY_LIST})`).option("--category <text>", "Finding category (e.g. Broken Access Control)").option("--cwe <id>", "CWE identifier (repeatable)", collectCwe).option(
|
|
47027
|
+
return new Command("create").description("Create a pentest finding from a contractor write-up (staff)").requiredOption("--pentest <id>", "Pentest display ID (e.g. PT-3) or UUID").option("--title <text>", "Finding title").option("--description <text>", "Finding description").option("--description-file <path>", "Read description from a file").option("--severity <severity>", `Severity (${SEVERITY_LIST})`).option("--category <text>", "Finding category (e.g. Broken Access Control)").option("--cwe <id>", "CWE identifier (repeatable)", collectCwe).option(
|
|
47028
|
+
"--cvss-vector <vector>",
|
|
47029
|
+
"CVSS base vector (CVSS:3.1/... or CVSS:4.0/...); the score is derived server-side"
|
|
47030
|
+
).option("--cvss-score <n>", "Ignored: the score is derived from --cvss-vector").option("--owasp <category>", "OWASP category").option("--poc <text>", "Proof of concept").option("--poc-file <path>", "Read proof of concept from a file").option("--remediation <text>", "Remediation guidance").option("--remediation-file <path>", "Read remediation from a file").option("--file <path>", "JSON object of finding fields; flags override").option("-o, --output <format>", "Output format: text, json (default: text)", "text").action(async (options) => {
|
|
46661
47031
|
await runCreateFinding(options);
|
|
46662
47032
|
});
|
|
46663
47033
|
}
|
|
46664
47034
|
|
|
46665
|
-
// src/pentest/
|
|
46666
|
-
import {
|
|
47035
|
+
// src/pentest/deliverables.ts
|
|
47036
|
+
import { open as open2, rename, rm, stat } from "fs/promises";
|
|
47037
|
+
import { basename as basename3, join as join6, resolve } from "path";
|
|
47038
|
+
|
|
47039
|
+
// src/upload/prepare.ts
|
|
47040
|
+
import { createHash } from "crypto";
|
|
47041
|
+
import { createReadStream, statSync as statSync3 } from "fs";
|
|
47042
|
+
import { open } from "fs/promises";
|
|
46667
47043
|
import { basename as basename2 } from "path";
|
|
46668
|
-
var
|
|
46669
|
-
|
|
46670
|
-
|
|
46671
|
-
|
|
47044
|
+
var CodeUploadPrepareError = class extends Error {
|
|
47045
|
+
constructor(message) {
|
|
47046
|
+
super(message);
|
|
47047
|
+
this.name = "CodeUploadPrepareError";
|
|
47048
|
+
}
|
|
47049
|
+
};
|
|
47050
|
+
var KIND_BY_INPUT = {
|
|
47051
|
+
source: "SOURCE_ARCHIVE",
|
|
47052
|
+
code: "SOURCE_ARCHIVE",
|
|
47053
|
+
"source-archive": "SOURCE_ARCHIVE",
|
|
47054
|
+
source_archive: "SOURCE_ARCHIVE",
|
|
47055
|
+
SOURCE_ARCHIVE: "SOURCE_ARCHIVE",
|
|
47056
|
+
apk: "ANDROID_APK",
|
|
47057
|
+
android: "ANDROID_APK",
|
|
47058
|
+
"android-apk": "ANDROID_APK",
|
|
47059
|
+
android_apk: "ANDROID_APK",
|
|
47060
|
+
ANDROID_APK: "ANDROID_APK",
|
|
47061
|
+
ipa: "IOS_IPA",
|
|
47062
|
+
ios: "IOS_IPA",
|
|
47063
|
+
"ios-ipa": "IOS_IPA",
|
|
47064
|
+
ios_ipa: "IOS_IPA",
|
|
47065
|
+
IOS_IPA: "IOS_IPA"
|
|
47066
|
+
};
|
|
47067
|
+
function normalizeArtifactKind(input) {
|
|
47068
|
+
const trimmed = input.trim();
|
|
47069
|
+
return KIND_BY_INPUT[trimmed] ?? KIND_BY_INPUT[trimmed.toLowerCase()] ?? null;
|
|
46672
47070
|
}
|
|
46673
|
-
function
|
|
46674
|
-
|
|
47071
|
+
function inferArtifactKindFromFilename(filename) {
|
|
47072
|
+
const lower = filename.toLowerCase();
|
|
47073
|
+
if (lower.endsWith(".zip")) return "SOURCE_ARCHIVE";
|
|
47074
|
+
if (lower.endsWith(".apk")) return "ANDROID_APK";
|
|
47075
|
+
if (lower.endsWith(".ipa")) return "IOS_IPA";
|
|
47076
|
+
return null;
|
|
46675
47077
|
}
|
|
46676
|
-
function
|
|
46677
|
-
if (
|
|
46678
|
-
if (
|
|
46679
|
-
return
|
|
47078
|
+
function requiredExtensionForKind(kind) {
|
|
47079
|
+
if (kind === "ANDROID_APK") return ".apk";
|
|
47080
|
+
if (kind === "IOS_IPA") return ".ipa";
|
|
47081
|
+
return ".zip";
|
|
46680
47082
|
}
|
|
46681
|
-
|
|
46682
|
-
|
|
46683
|
-
|
|
47083
|
+
function defaultDisplayName(originalFilename) {
|
|
47084
|
+
const base = originalFilename.replace(/\.(zip|apk|ipa)$/i, "").trim();
|
|
47085
|
+
return base.slice(0, 200) || "source";
|
|
47086
|
+
}
|
|
47087
|
+
function formatArchiveBytes(bytes) {
|
|
47088
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
47089
|
+
if (bytes < 1024 * 1024) {
|
|
47090
|
+
const kb = bytes / 1024;
|
|
47091
|
+
return `${kb.toFixed(Number.isInteger(kb) ? 0 : 1)} KB`;
|
|
46684
47092
|
}
|
|
46685
|
-
|
|
46686
|
-
|
|
46687
|
-
|
|
46688
|
-
colorError(
|
|
46689
|
-
`Invalid finding identifier '${sanitizeDisplay(identifier)}'. Use a finding number (e.g. PTF-7) or UUID.`
|
|
46690
|
-
)
|
|
46691
|
-
);
|
|
46692
|
-
process.exit(1);
|
|
46693
|
-
}
|
|
46694
|
-
const displayId = Number.parseInt(numericMatch[1], 10);
|
|
46695
|
-
const result = await authenticatedRequest(
|
|
46696
|
-
(client) => client.pentestFinding.listFindings({ params: { organizationId, pentestId } })
|
|
46697
|
-
);
|
|
46698
|
-
if (result.status === UNAUTHORIZED_STATUS2) {
|
|
46699
|
-
exitAuthFailed();
|
|
47093
|
+
if (bytes < 1024 * 1024 * 1024) {
|
|
47094
|
+
const mb = bytes / (1024 * 1024);
|
|
47095
|
+
return `${mb.toFixed(Number.isInteger(mb) ? 0 : 1)} MB`;
|
|
46700
47096
|
}
|
|
46701
|
-
|
|
46702
|
-
|
|
46703
|
-
|
|
47097
|
+
const gb = bytes / (1024 * 1024 * 1024);
|
|
47098
|
+
return `${gb.toFixed(Number.isInteger(gb) ? 0 : 1)} GB`;
|
|
47099
|
+
}
|
|
47100
|
+
function archiveTooLargeMessage() {
|
|
47101
|
+
return `The file must be ${formatArchiveBytes(CODE_UPLOAD_MAX_BYTES)} or smaller.`;
|
|
47102
|
+
}
|
|
47103
|
+
async function hashFileSha256(filePath) {
|
|
47104
|
+
const hash2 = createHash("sha256");
|
|
47105
|
+
const stream = createReadStream(filePath);
|
|
47106
|
+
for await (const chunk of stream) {
|
|
47107
|
+
hash2.update(chunk);
|
|
46704
47108
|
}
|
|
46705
|
-
|
|
46706
|
-
|
|
46707
|
-
|
|
46708
|
-
|
|
46709
|
-
|
|
46710
|
-
|
|
47109
|
+
return hash2.digest("hex");
|
|
47110
|
+
}
|
|
47111
|
+
async function readFileSlice(filePath, start, endExclusive) {
|
|
47112
|
+
const length = endExclusive - start;
|
|
47113
|
+
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(length) || start < 0 || length < 1) {
|
|
47114
|
+
throw new Error(
|
|
47115
|
+
`Invalid file slice: start=${String(start)} endExclusive=${String(endExclusive)}.`
|
|
46711
47116
|
);
|
|
46712
|
-
process.exit(1);
|
|
46713
47117
|
}
|
|
46714
|
-
|
|
46715
|
-
|
|
46716
|
-
|
|
46717
|
-
|
|
46718
|
-
|
|
46719
|
-
|
|
46720
|
-
|
|
47118
|
+
const buffer = Buffer.alloc(length);
|
|
47119
|
+
const handle = await open(filePath, "r");
|
|
47120
|
+
try {
|
|
47121
|
+
const { bytesRead } = await handle.read(buffer, 0, length, start);
|
|
47122
|
+
if (bytesRead !== length) {
|
|
47123
|
+
throw new Error(
|
|
47124
|
+
`Expected to read ${String(length)} bytes at offset ${String(start)}, got ${String(bytesRead)}.`
|
|
47125
|
+
);
|
|
47126
|
+
}
|
|
47127
|
+
return buffer;
|
|
47128
|
+
} finally {
|
|
47129
|
+
await handle.close();
|
|
46721
47130
|
}
|
|
46722
|
-
|
|
46723
|
-
|
|
46724
|
-
|
|
46725
|
-
|
|
47131
|
+
}
|
|
47132
|
+
function assertPreparedMatchesUpload(upload, prepared) {
|
|
47133
|
+
if (upload.sizeBytes !== prepared.sizeBytes || upload.archiveSha256 !== prepared.archiveSha256) {
|
|
47134
|
+
throw new CodeUploadPrepareError(
|
|
47135
|
+
"The file does not match the interrupted upload (size or SHA-256 differs)."
|
|
47136
|
+
);
|
|
46726
47137
|
}
|
|
47138
|
+
}
|
|
47139
|
+
async function prepareUploadFile(filePath, options = {}) {
|
|
46727
47140
|
let sizeBytes;
|
|
46728
47141
|
try {
|
|
46729
|
-
const stats = statSync3(
|
|
47142
|
+
const stats = statSync3(filePath);
|
|
46730
47143
|
if (!stats.isFile()) {
|
|
46731
|
-
|
|
46732
|
-
process.exit(1);
|
|
47144
|
+
throw new CodeUploadPrepareError("Path is not a regular file. Pass a .zip, .apk, or .ipa.");
|
|
46733
47145
|
}
|
|
46734
47146
|
sizeBytes = stats.size;
|
|
46735
|
-
} catch {
|
|
46736
|
-
|
|
46737
|
-
|
|
46738
|
-
}
|
|
46739
|
-
if (sizeBytes === 0) {
|
|
46740
|
-
console.error(colorError(`--file '${sanitizeDisplay(options.file)}' is empty.`));
|
|
46741
|
-
process.exit(1);
|
|
46742
|
-
}
|
|
46743
|
-
if (sizeBytes > MAX_STAFF_EVIDENCE_UPLOAD_BYTES) {
|
|
46744
|
-
console.error(
|
|
46745
|
-
colorError(
|
|
46746
|
-
`File is ${formatBytes2(sizeBytes)}; the limit for one exhibit is ${formatBytes2(MAX_STAFF_EVIDENCE_UPLOAD_BYTES)}.`
|
|
46747
|
-
)
|
|
46748
|
-
);
|
|
46749
|
-
process.exit(1);
|
|
46750
|
-
}
|
|
46751
|
-
const organizationId = await resolveOrganizationId();
|
|
46752
|
-
const pentestId = await resolvePentestId(organizationId, options.pentest);
|
|
46753
|
-
const pentestFindingId = await resolveFindingId(organizationId, pentestId, options.finding);
|
|
46754
|
-
const params = { organizationId, pentestId, pentestFindingId };
|
|
46755
|
-
const filename = basename2(options.file);
|
|
46756
|
-
const slot = await authenticatedRequest(
|
|
46757
|
-
(client) => client.pentestFinding.createFindingEvidenceUpload({
|
|
46758
|
-
params,
|
|
46759
|
-
body: { filename, sizeBytes }
|
|
46760
|
-
})
|
|
46761
|
-
);
|
|
46762
|
-
if (slot.status === UNAUTHORIZED_STATUS2) {
|
|
46763
|
-
exitAuthFailed();
|
|
46764
|
-
}
|
|
46765
|
-
if (slot.status !== 201) {
|
|
46766
|
-
const { message } = readErrorFields(slot.body);
|
|
46767
|
-
console.error(
|
|
46768
|
-
colorError(message ?? `Failed to start the evidence upload (status ${slot.status}).`)
|
|
46769
|
-
);
|
|
46770
|
-
process.exit(1);
|
|
47147
|
+
} catch (error48) {
|
|
47148
|
+
if (error48 instanceof CodeUploadPrepareError) throw error48;
|
|
47149
|
+
throw new CodeUploadPrepareError("Cannot read that file.");
|
|
46771
47150
|
}
|
|
46772
|
-
|
|
46773
|
-
|
|
46774
|
-
if (!put.ok) {
|
|
46775
|
-
console.error(
|
|
46776
|
-
colorError(
|
|
46777
|
-
`Upload to storage failed (status ${put.status}). The upload link is short-lived \u2014 re-run the command.`
|
|
46778
|
-
)
|
|
46779
|
-
);
|
|
46780
|
-
process.exit(1);
|
|
47151
|
+
if (!Number.isSafeInteger(sizeBytes) || sizeBytes < 1) {
|
|
47152
|
+
throw new CodeUploadPrepareError("The file is empty.");
|
|
46781
47153
|
}
|
|
46782
|
-
|
|
46783
|
-
|
|
46784
|
-
params,
|
|
46785
|
-
body: {
|
|
46786
|
-
key: slot.body.key,
|
|
46787
|
-
...options.caption !== void 0 ? { caption: options.caption } : {},
|
|
46788
|
-
...options.section !== void 0 && isSection(options.section) ? { section: options.section } : {},
|
|
46789
|
-
...options.kind !== void 0 && isKind(options.kind) ? { kind: options.kind } : {},
|
|
46790
|
-
...options.customerFacing ? { customerFacing: true } : {}
|
|
46791
|
-
}
|
|
46792
|
-
})
|
|
46793
|
-
);
|
|
46794
|
-
if (attached.status === UNAUTHORIZED_STATUS2) {
|
|
46795
|
-
exitAuthFailed();
|
|
47154
|
+
if (sizeBytes > CODE_UPLOAD_MAX_BYTES) {
|
|
47155
|
+
throw new CodeUploadPrepareError(archiveTooLargeMessage());
|
|
46796
47156
|
}
|
|
46797
|
-
|
|
46798
|
-
|
|
46799
|
-
|
|
46800
|
-
|
|
46801
|
-
);
|
|
46802
|
-
process.exit(1);
|
|
47157
|
+
const originalFilename = basename2(filePath);
|
|
47158
|
+
const inferred = inferArtifactKindFromFilename(originalFilename);
|
|
47159
|
+
const artifactKind = options.artifactKind ?? inferred;
|
|
47160
|
+
if (artifactKind == null) {
|
|
47161
|
+
throw new CodeUploadPrepareError("Pass a .zip source archive, an .apk, or an .ipa.");
|
|
46803
47162
|
}
|
|
46804
|
-
|
|
46805
|
-
|
|
46806
|
-
|
|
47163
|
+
const expected = requiredExtensionForKind(artifactKind);
|
|
47164
|
+
if (!originalFilename.toLowerCase().endsWith(expected)) {
|
|
47165
|
+
const label = isApplicationBuildArtifactKind(artifactKind) ? artifactKind === "ANDROID_APK" ? "Android APK" : "iOS package" : "source archive";
|
|
47166
|
+
throw new CodeUploadPrepareError(`A ${label} must end in ${expected}.`);
|
|
46807
47167
|
}
|
|
46808
|
-
const
|
|
46809
|
-
|
|
46810
|
-
|
|
46811
|
-
colorSuccess(
|
|
46812
|
-
`Attached ${attached.body.kind} \xB7 ${formatBytes2(attached.body.sizeBytes)} \xB7 ${visibility}${placement}`
|
|
46813
|
-
)
|
|
46814
|
-
);
|
|
46815
|
-
console.log(` artifact ${attached.body.artifactId}`);
|
|
46816
|
-
if (attached.body.kind === "HAR") {
|
|
46817
|
-
console.log(" HAR captures stay internal \u2014 they can carry a live session token.");
|
|
46818
|
-
} else if (!attached.body.customerFacing) {
|
|
46819
|
-
console.log(" Publish it from the finding\u2019s evidence panel in the staff console.");
|
|
47168
|
+
const displayName = (options.displayName ?? defaultDisplayName(originalFilename)).trim();
|
|
47169
|
+
if (displayName.length < 1 || displayName.length > 200) {
|
|
47170
|
+
throw new CodeUploadPrepareError("Display name must be 1 to 200 characters.");
|
|
46820
47171
|
}
|
|
46821
|
-
|
|
46822
|
-
|
|
46823
|
-
|
|
46824
|
-
|
|
46825
|
-
|
|
46826
|
-
|
|
46827
|
-
|
|
46828
|
-
|
|
46829
|
-
|
|
46830
|
-
Examples:
|
|
46831
|
-
tracecli pentest findings attach-evidence --pentest PT-3 --finding 7 --file ./poc.png
|
|
46832
|
-
tracecli pentest findings attach-evidence --pentest PT-3 --finding PTF-7 --file ./poc.mov \\
|
|
46833
|
-
--caption "Cookie survives logout, replayed 40s later" --section proofOfConcept
|
|
46834
|
-
tracecli pentest findings attach-evidence --pentest PT-3 --finding 7 --file ./roster.json \\
|
|
46835
|
-
--caption "Full member roster returned to a non-member" --customer-facing --output json`
|
|
46836
|
-
).action(async (options) => {
|
|
46837
|
-
await runAttachEvidence(options);
|
|
46838
|
-
});
|
|
47172
|
+
return {
|
|
47173
|
+
path: filePath,
|
|
47174
|
+
displayName,
|
|
47175
|
+
originalFilename,
|
|
47176
|
+
sizeBytes,
|
|
47177
|
+
archiveSha256: await hashFileSha256(filePath),
|
|
47178
|
+
artifactKind
|
|
47179
|
+
};
|
|
46839
47180
|
}
|
|
46840
47181
|
|
|
46841
47182
|
// src/pentest/status-update.ts
|
|
@@ -46954,68 +47295,738 @@ async function runStatusUpdate(options) {
|
|
|
46954
47295
|
}
|
|
46955
47296
|
if (note !== void 0 && target === "IN_PROGRESS") {
|
|
46956
47297
|
console.error(
|
|
46957
|
-
colorError("--note is only used when reporting remediation (--status REMEDIATED).")
|
|
47298
|
+
colorError("--note is only used when reporting remediation (--status REMEDIATED).")
|
|
47299
|
+
);
|
|
47300
|
+
process.exit(1);
|
|
47301
|
+
}
|
|
47302
|
+
const viaApiKey = requireAuth().kind === "api_key";
|
|
47303
|
+
const organizationId = await resolveOrganizationId();
|
|
47304
|
+
const params = {
|
|
47305
|
+
organizationId,
|
|
47306
|
+
pentestId: options.pentest,
|
|
47307
|
+
pentestFindingId: options.finding
|
|
47308
|
+
};
|
|
47309
|
+
const result = viaApiKey ? await authenticatedRequest(
|
|
47310
|
+
(client) => client.pentestFinding.automationStatusUpdate({
|
|
47311
|
+
params,
|
|
47312
|
+
body: { status: target, ...note !== void 0 ? { note } : {} }
|
|
47313
|
+
})
|
|
47314
|
+
) : target === "IN_PROGRESS" ? await authenticatedRequest(
|
|
47315
|
+
(client) => client.pentestFinding.startProgress({ params, body: {} })
|
|
47316
|
+
) : await authenticatedRequest(
|
|
47317
|
+
(client) => client.pentestFinding.reportRemediation({
|
|
47318
|
+
params,
|
|
47319
|
+
body: note !== void 0 ? { note } : {}
|
|
47320
|
+
})
|
|
47321
|
+
);
|
|
47322
|
+
if (result.status !== 200) {
|
|
47323
|
+
failStatusUpdate(result.status, result.body, viaApiKey);
|
|
47324
|
+
}
|
|
47325
|
+
if (options.output === "json") {
|
|
47326
|
+
console.log(JSON.stringify(result.body, null, 2));
|
|
47327
|
+
return;
|
|
47328
|
+
}
|
|
47329
|
+
const displayId = result.body.displayId;
|
|
47330
|
+
const status = sanitizeDisplay(result.body.status);
|
|
47331
|
+
console.log(colorSuccess(`PTF-${displayId} is now ${status}`));
|
|
47332
|
+
}
|
|
47333
|
+
function createStatusUpdateCommand() {
|
|
47334
|
+
return new Command("status-update").description("Move a pentest finding to IN_PROGRESS or REMEDIATED").requiredOption("--pentest <id>", "Pentest display ID (e.g. PT-3) or UUID").requiredOption("--finding <id>", "Finding number (e.g. PTF-7) or UUID").requiredOption(
|
|
47335
|
+
"--status <status>",
|
|
47336
|
+
"IN_PROGRESS (start work) or REMEDIATED (ready for retest)"
|
|
47337
|
+
).option("--note <text>", "Optional context when reporting remediation").option("-o, --output <format>", "Output format: text, json (default: text)", "text").addHelpText(
|
|
47338
|
+
"after",
|
|
47339
|
+
`
|
|
47340
|
+
Customer-facing status writes \u2014 the same transitions as the dashboard. An agent
|
|
47341
|
+
can mark a finding in progress when it starts the fix, then remediated when the
|
|
47342
|
+
fix is ready for Trace to re-test. Leaving the finding IN_PROGRESS (and not
|
|
47343
|
+
sending REMEDIATED) keeps it out of the retest queue.
|
|
47344
|
+
|
|
47345
|
+
IN_PROGRESS is allowed from OPEN or VERIFIED_OPEN. REMEDIATED is allowed from
|
|
47346
|
+
OPEN, IN_PROGRESS, or VERIFIED_OPEN.
|
|
47347
|
+
|
|
47348
|
+
A signed-in session (\`tracecli auth login\`) attributes the change to you. A
|
|
47349
|
+
Trace API key works too, once Trace enables finding automation for your
|
|
47350
|
+
organization: those writes are recorded as \`api-key:<key name>\` and show up as
|
|
47351
|
+
yours in the finding's history.
|
|
47352
|
+
|
|
47353
|
+
Examples:
|
|
47354
|
+
tracecli pentest findings status-update --pentest PT-3 --finding 7 --status IN_PROGRESS
|
|
47355
|
+
tracecli pentest findings status-update --pentest PT-3 --finding PTF-7 --status REMEDIATED
|
|
47356
|
+
tracecli pentest findings status-update --pentest PT-3 --finding 7 --status REMEDIATED \\
|
|
47357
|
+
--note "Fixed in PR 412" --output json`
|
|
47358
|
+
).action(async (options) => {
|
|
47359
|
+
await runStatusUpdate(options);
|
|
47360
|
+
});
|
|
47361
|
+
}
|
|
47362
|
+
|
|
47363
|
+
// src/pentest/deliverables.ts
|
|
47364
|
+
var PentestDeliverableDownloadError = class extends Error {
|
|
47365
|
+
constructor(message) {
|
|
47366
|
+
super(message);
|
|
47367
|
+
this.name = "PentestDeliverableDownloadError";
|
|
47368
|
+
}
|
|
47369
|
+
};
|
|
47370
|
+
function safeDeliverableFilename(documentId, extension, fallbackStem) {
|
|
47371
|
+
const stripped = Array.from(basename3(documentId.replace(/\\/g, "/")), (char) => {
|
|
47372
|
+
const code = char.codePointAt(0) ?? 0;
|
|
47373
|
+
return code <= 31 || code === 127 ? "_" : char;
|
|
47374
|
+
}).join("").trim();
|
|
47375
|
+
const stem = stripped.length === 0 || stripped === "." || stripped === ".." ? fallbackStem : stripped;
|
|
47376
|
+
return stem.toLowerCase().endsWith(extension) ? stem : `${stem}${extension}`;
|
|
47377
|
+
}
|
|
47378
|
+
function reportFileName(report, artifact) {
|
|
47379
|
+
if (artifact === "docx") {
|
|
47380
|
+
return safeDeliverableFilename(report.documentId, ".docx", "pentest-report");
|
|
47381
|
+
}
|
|
47382
|
+
const extension = report.downloadFormat === "bundle" ? ".zip" : ".pdf";
|
|
47383
|
+
return safeDeliverableFilename(report.documentId, extension, "pentest-report");
|
|
47384
|
+
}
|
|
47385
|
+
function attestationFileName(attestation) {
|
|
47386
|
+
return safeDeliverableFilename(attestation.documentId, ".pdf", "letter-of-attestation");
|
|
47387
|
+
}
|
|
47388
|
+
async function resolveDeliverableDestination(fileName, outOption) {
|
|
47389
|
+
if (outOption === void 0) return resolve(process.cwd(), fileName);
|
|
47390
|
+
const target = resolve(outOption);
|
|
47391
|
+
try {
|
|
47392
|
+
const info = await stat(target);
|
|
47393
|
+
if (info.isDirectory()) return join6(target, fileName);
|
|
47394
|
+
} catch {
|
|
47395
|
+
}
|
|
47396
|
+
return target;
|
|
47397
|
+
}
|
|
47398
|
+
async function pathExists(path) {
|
|
47399
|
+
try {
|
|
47400
|
+
await stat(path);
|
|
47401
|
+
return true;
|
|
47402
|
+
} catch {
|
|
47403
|
+
return false;
|
|
47404
|
+
}
|
|
47405
|
+
}
|
|
47406
|
+
async function downloadDeliverableToFile(downloadUrl, destination, options = {}) {
|
|
47407
|
+
const fetchImpl = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
|
|
47408
|
+
const partialPath = `${destination}.part`;
|
|
47409
|
+
let response;
|
|
47410
|
+
try {
|
|
47411
|
+
response = await fetchImpl(downloadUrl, { method: "GET", redirect: "manual" });
|
|
47412
|
+
} catch (error48) {
|
|
47413
|
+
const detail = error48 instanceof Error ? error48.message : "network error";
|
|
47414
|
+
throw new PentestDeliverableDownloadError(`Download from storage failed: ${detail}.`);
|
|
47415
|
+
}
|
|
47416
|
+
if (!response.ok || response.body === null) {
|
|
47417
|
+
if (response.body !== null) await response.body.cancel();
|
|
47418
|
+
throw new PentestDeliverableDownloadError(
|
|
47419
|
+
`Storage answered HTTP ${response.status} instead of the document. Retry; the download link may have expired.`
|
|
47420
|
+
);
|
|
47421
|
+
}
|
|
47422
|
+
const contentLength = response.headers.get("content-length");
|
|
47423
|
+
let totalBytes = null;
|
|
47424
|
+
if (contentLength !== null) {
|
|
47425
|
+
const declared = Number(contentLength);
|
|
47426
|
+
if (!Number.isSafeInteger(declared) || declared < 0) {
|
|
47427
|
+
await response.body.cancel();
|
|
47428
|
+
throw new PentestDeliverableDownloadError(
|
|
47429
|
+
`Storage declared an unreadable Content-Length ('${contentLength}').`
|
|
47430
|
+
);
|
|
47431
|
+
}
|
|
47432
|
+
totalBytes = declared;
|
|
47433
|
+
}
|
|
47434
|
+
await rm(partialPath, { force: true });
|
|
47435
|
+
const file2 = await open2(partialPath, "wx");
|
|
47436
|
+
const reader = response.body.getReader();
|
|
47437
|
+
let downloadedBytes = 0;
|
|
47438
|
+
options.onProgress?.({ downloadedBytes, totalBytes });
|
|
47439
|
+
try {
|
|
47440
|
+
for (; ; ) {
|
|
47441
|
+
const chunk = await reader.read();
|
|
47442
|
+
if (chunk.done) break;
|
|
47443
|
+
downloadedBytes += chunk.value.byteLength;
|
|
47444
|
+
if (totalBytes !== null && downloadedBytes > totalBytes) {
|
|
47445
|
+
await reader.cancel();
|
|
47446
|
+
throw new PentestDeliverableDownloadError(
|
|
47447
|
+
`Storage sent more than the ${totalBytes} bytes it declared.`
|
|
47448
|
+
);
|
|
47449
|
+
}
|
|
47450
|
+
await writeAll(file2, chunk.value);
|
|
47451
|
+
options.onProgress?.({ downloadedBytes, totalBytes });
|
|
47452
|
+
}
|
|
47453
|
+
await file2.close();
|
|
47454
|
+
if (totalBytes !== null && downloadedBytes !== totalBytes) {
|
|
47455
|
+
throw new PentestDeliverableDownloadError(
|
|
47456
|
+
`Download ended after ${downloadedBytes} of ${totalBytes} bytes.`
|
|
47457
|
+
);
|
|
47458
|
+
}
|
|
47459
|
+
if (downloadedBytes === 0) {
|
|
47460
|
+
throw new PentestDeliverableDownloadError("Storage returned an empty document.");
|
|
47461
|
+
}
|
|
47462
|
+
await rename(partialPath, destination);
|
|
47463
|
+
return { bytes: downloadedBytes };
|
|
47464
|
+
} catch (error48) {
|
|
47465
|
+
await file2.close().catch(() => void 0);
|
|
47466
|
+
await rm(partialPath, { force: true });
|
|
47467
|
+
if (error48 instanceof PentestDeliverableDownloadError) throw error48;
|
|
47468
|
+
const detail = error48 instanceof Error ? error48.message : "unknown error";
|
|
47469
|
+
throw new PentestDeliverableDownloadError(`Download from storage failed: ${detail}.`);
|
|
47470
|
+
}
|
|
47471
|
+
}
|
|
47472
|
+
async function writeAll(file2, bytes) {
|
|
47473
|
+
let offset = 0;
|
|
47474
|
+
while (offset < bytes.byteLength) {
|
|
47475
|
+
const { bytesWritten } = await file2.write(bytes, offset, bytes.byteLength - offset);
|
|
47476
|
+
if (bytesWritten === 0) {
|
|
47477
|
+
throw new PentestDeliverableDownloadError("Writing the document to disk stalled.");
|
|
47478
|
+
}
|
|
47479
|
+
offset += bytesWritten;
|
|
47480
|
+
}
|
|
47481
|
+
}
|
|
47482
|
+
var DELIVERABLE_LABEL = {
|
|
47483
|
+
report: "report",
|
|
47484
|
+
attestation: "letter of attestation"
|
|
47485
|
+
};
|
|
47486
|
+
function requirePentestIdentifier(value) {
|
|
47487
|
+
if (!isPentestIdentifier(value)) {
|
|
47488
|
+
console.error(
|
|
47489
|
+
colorError(`Invalid pentest identifier '${value}'. Use a display ID (e.g. PT-3) or UUID.`)
|
|
47490
|
+
);
|
|
47491
|
+
process.exit(1);
|
|
47492
|
+
}
|
|
47493
|
+
}
|
|
47494
|
+
function failDeliverable(status, body, deliverable, pentestIdentifier, action) {
|
|
47495
|
+
const { message, errorCode } = readErrorFields(body);
|
|
47496
|
+
const label = DELIVERABLE_LABEL[deliverable];
|
|
47497
|
+
if (status === UNAUTHORIZED_STATUS2) {
|
|
47498
|
+
exitAuthFailed();
|
|
47499
|
+
}
|
|
47500
|
+
if (status === 403) {
|
|
47501
|
+
console.error(
|
|
47502
|
+
colorError(
|
|
47503
|
+
message ?? `You do not have permission to ${action === "list" ? "view" : "download"} ${label}s for this organization.`
|
|
47504
|
+
)
|
|
47505
|
+
);
|
|
47506
|
+
process.exit(1);
|
|
47507
|
+
}
|
|
47508
|
+
if (status === 404) {
|
|
47509
|
+
if (message === "Report not found") {
|
|
47510
|
+
console.error(
|
|
47511
|
+
colorError(
|
|
47512
|
+
`No report has been shared for pentest ${pentestIdentifier} yet. Trace publishes it to your dashboard when the engagement's report is ready.`
|
|
47513
|
+
)
|
|
47514
|
+
);
|
|
47515
|
+
process.exit(1);
|
|
47516
|
+
}
|
|
47517
|
+
if (message === "Attestation not found") {
|
|
47518
|
+
console.error(
|
|
47519
|
+
colorError(
|
|
47520
|
+
`That letter of attestation is not available for pentest ${pentestIdentifier}. Run \`tracecli pentest attestation list ${pentestIdentifier}\` to see the letters shared with you.`
|
|
47521
|
+
)
|
|
47522
|
+
);
|
|
47523
|
+
process.exit(1);
|
|
47524
|
+
}
|
|
47525
|
+
console.error(
|
|
47526
|
+
colorError(
|
|
47527
|
+
`Pentest '${pentestIdentifier}' not found. Verify the ID with \`tracecli pentest list\`.`
|
|
47528
|
+
)
|
|
47529
|
+
);
|
|
47530
|
+
process.exit(1);
|
|
47531
|
+
}
|
|
47532
|
+
if (status === 409) {
|
|
47533
|
+
if (errorCode === "DOCX_NOT_RENDERED") {
|
|
47534
|
+
console.error(
|
|
47535
|
+
colorError(
|
|
47536
|
+
"No Word document is available for this report. Drop --docx to download the PDF."
|
|
47537
|
+
)
|
|
47538
|
+
);
|
|
47539
|
+
process.exit(1);
|
|
47540
|
+
}
|
|
47541
|
+
console.error(colorError(message ?? `Conflict (status ${status}).`));
|
|
47542
|
+
process.exit(1);
|
|
47543
|
+
}
|
|
47544
|
+
if (status === 400) {
|
|
47545
|
+
console.error(colorError(message ?? "Invalid request (status 400)."));
|
|
47546
|
+
process.exit(1);
|
|
47547
|
+
}
|
|
47548
|
+
console.error(
|
|
47549
|
+
colorError(
|
|
47550
|
+
`Failed to ${action} ${label}${action === "list" ? "s" : ""} (status ${status})${message ? `: ${message}` : "."}`
|
|
47551
|
+
)
|
|
47552
|
+
);
|
|
47553
|
+
process.exit(1);
|
|
47554
|
+
}
|
|
47555
|
+
function writeDownloadProgress(progress3) {
|
|
47556
|
+
if (!process.stderr.isTTY) return;
|
|
47557
|
+
const downloaded = formatArchiveBytes(progress3.downloadedBytes);
|
|
47558
|
+
if (progress3.totalBytes === null || progress3.totalBytes === 0) {
|
|
47559
|
+
process.stderr.write(`\rDownloading ${downloaded}`);
|
|
47560
|
+
return;
|
|
47561
|
+
}
|
|
47562
|
+
const pct = Math.floor(progress3.downloadedBytes / progress3.totalBytes * 100);
|
|
47563
|
+
process.stderr.write(
|
|
47564
|
+
`\rDownloading ${pct}% (${downloaded} / ${formatArchiveBytes(progress3.totalBytes)})`
|
|
47565
|
+
);
|
|
47566
|
+
}
|
|
47567
|
+
function finishProgress() {
|
|
47568
|
+
if (process.stderr.isTTY) process.stderr.write("\n");
|
|
47569
|
+
}
|
|
47570
|
+
async function saveDeliverable(downloadUrl, fileName, options) {
|
|
47571
|
+
const destination = await resolveDeliverableDestination(fileName, options.out);
|
|
47572
|
+
if (!options.force && await pathExists(destination)) {
|
|
47573
|
+
console.error(
|
|
47574
|
+
colorError(`${destination} already exists. Pass --force to overwrite it or --out <path>.`)
|
|
47575
|
+
);
|
|
47576
|
+
process.exit(1);
|
|
47577
|
+
}
|
|
47578
|
+
if (options.output === "table") {
|
|
47579
|
+
console.error(`Downloading ${sanitizeDisplay(fileName)} to ${destination}`);
|
|
47580
|
+
}
|
|
47581
|
+
try {
|
|
47582
|
+
const { bytes } = await downloadDeliverableToFile(downloadUrl, destination, {
|
|
47583
|
+
onProgress: writeDownloadProgress
|
|
47584
|
+
});
|
|
47585
|
+
finishProgress();
|
|
47586
|
+
return { destination, bytes };
|
|
47587
|
+
} catch (error48) {
|
|
47588
|
+
finishProgress();
|
|
47589
|
+
const detail = error48 instanceof Error ? error48.message : "Download from storage failed.";
|
|
47590
|
+
console.error(colorError(detail));
|
|
47591
|
+
process.exit(1);
|
|
47592
|
+
}
|
|
47593
|
+
}
|
|
47594
|
+
async function runReportDownload(pentestIdentifier, options) {
|
|
47595
|
+
const output = parseOutput(options.output);
|
|
47596
|
+
requirePentestIdentifier(pentestIdentifier);
|
|
47597
|
+
const artifact = options.docx ? "docx" : "primary";
|
|
47598
|
+
const organizationId = await resolveOrganizationId();
|
|
47599
|
+
const result = await authenticatedRequest(
|
|
47600
|
+
(client) => client.pentestReportPdf.downloadCustomer({
|
|
47601
|
+
params: { organizationId, pentestId: pentestIdentifier },
|
|
47602
|
+
query: {
|
|
47603
|
+
disposition: "attachment",
|
|
47604
|
+
...artifact === "docx" ? { artifact: "docx" } : {}
|
|
47605
|
+
}
|
|
47606
|
+
})
|
|
47607
|
+
);
|
|
47608
|
+
if (result.status !== SUCCESS_STATUS) {
|
|
47609
|
+
failDeliverable(result.status, result.body, "report", pentestIdentifier, "download");
|
|
47610
|
+
}
|
|
47611
|
+
const { report, downloadUrl } = result.body;
|
|
47612
|
+
const fileName = reportFileName(report, artifact);
|
|
47613
|
+
const { destination, bytes } = await saveDeliverable(downloadUrl, fileName, {
|
|
47614
|
+
...options.out !== void 0 ? { out: options.out } : {},
|
|
47615
|
+
...options.force !== void 0 ? { force: options.force } : {},
|
|
47616
|
+
output
|
|
47617
|
+
});
|
|
47618
|
+
if (output === "json") {
|
|
47619
|
+
console.log(JSON.stringify({ report, artifact, path: destination, sizeBytes: bytes }, null, 2));
|
|
47620
|
+
return;
|
|
47621
|
+
}
|
|
47622
|
+
if (output === "tsv") {
|
|
47623
|
+
console.log(["documentId", "version", "artifact", "path", "sizeBytes"].join(" "));
|
|
47624
|
+
console.log(
|
|
47625
|
+
[
|
|
47626
|
+
tsvSafe(sanitizeDisplay(report.documentId)),
|
|
47627
|
+
tsvSafe(sanitizeDisplay(report.version)),
|
|
47628
|
+
artifact,
|
|
47629
|
+
tsvSafe(destination),
|
|
47630
|
+
String(bytes)
|
|
47631
|
+
].join(" ")
|
|
47632
|
+
);
|
|
47633
|
+
return;
|
|
47634
|
+
}
|
|
47635
|
+
console.log(
|
|
47636
|
+
colorSuccess(
|
|
47637
|
+
`Saved ${sanitizeDisplay(report.documentId)} (${sanitizeDisplay(report.version)}) to ${destination} (${formatArchiveBytes(bytes)}).`
|
|
47638
|
+
)
|
|
47639
|
+
);
|
|
47640
|
+
}
|
|
47641
|
+
function createReportDownloadCommand() {
|
|
47642
|
+
return new Command("download").description("Download the pentest report shared with your organization").argument("<pentestId>", "Pentest display ID (e.g. PT-3) or UUID").option("--out <path>", "File to write, or an existing directory (default: working directory)").option("-f, --force", "Overwrite an existing file").option("--docx", "Download the Word document instead of the PDF (when your report ships one)").option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").addHelpText(
|
|
47643
|
+
"after",
|
|
47644
|
+
`
|
|
47645
|
+
Downloads the report Trace has published for the engagement: the issued version
|
|
47646
|
+
when one exists, otherwise the current shared draft. The file is named after the
|
|
47647
|
+
report's document ID (for example TR-PT-2026-03-ACME.pdf). Organizations on
|
|
47648
|
+
a partner report template receive a zip carrying the PDF, the Word document, the
|
|
47649
|
+
markdown report, and evidence files.
|
|
47650
|
+
|
|
47651
|
+
Works with a browser session from \`tracecli auth login\` or a Trace API key
|
|
47652
|
+
(TRACE_API_KEY), so it can run unattended in CI.
|
|
47653
|
+
|
|
47654
|
+
Examples:
|
|
47655
|
+
tracecli pentest report download PT-3
|
|
47656
|
+
tracecli pentest report download PT-3 --out ./reports/
|
|
47657
|
+
tracecli pentest report download PT-3 --out ./acme-q3.pdf --force
|
|
47658
|
+
tracecli pentest report download PT-3 --docx
|
|
47659
|
+
tracecli pentest report download PT-3 --output json | jq '.path'`
|
|
47660
|
+
).action(async (pentestId, options) => {
|
|
47661
|
+
await runReportDownload(pentestId, options);
|
|
47662
|
+
});
|
|
47663
|
+
}
|
|
47664
|
+
function createReportCommand() {
|
|
47665
|
+
const report = new Command("report").description("Download the pentest report");
|
|
47666
|
+
report.addCommand(createReportDownloadCommand());
|
|
47667
|
+
return report;
|
|
47668
|
+
}
|
|
47669
|
+
function sortAttestationsNewestFirst(attestations) {
|
|
47670
|
+
return [...attestations].sort((a2, b) => {
|
|
47671
|
+
const delta = new Date(b.createdAt).getTime() - new Date(a2.createdAt).getTime();
|
|
47672
|
+
return delta !== 0 ? delta : a2.attestationId.localeCompare(b.attestationId);
|
|
47673
|
+
});
|
|
47674
|
+
}
|
|
47675
|
+
function renderAttestationLines(attestation) {
|
|
47676
|
+
const counts = attestation.severityCounts;
|
|
47677
|
+
return [
|
|
47678
|
+
` ${sanitizeDisplay(attestation.documentId)} (${sanitizeDisplay(attestation.version)}, ${sanitizeDisplay(attestation.classification)})`,
|
|
47679
|
+
` Issued: ${formatDate(attestation.issuedDate)} | Findings: ${attestation.totalFindings} (C${counts.critical} H${counts.high} M${counts.medium} L${counts.low} I${counts.info}) | ${formatArchiveBytes(attestation.fileSizeBytes)}`,
|
|
47680
|
+
` ID: ${attestation.attestationId}`,
|
|
47681
|
+
""
|
|
47682
|
+
];
|
|
47683
|
+
}
|
|
47684
|
+
var ATTESTATION_TSV_HEADER = [
|
|
47685
|
+
"attestationId",
|
|
47686
|
+
"documentId",
|
|
47687
|
+
"version",
|
|
47688
|
+
"classification",
|
|
47689
|
+
"issuedDate",
|
|
47690
|
+
"totalFindings",
|
|
47691
|
+
"fileSizeBytes",
|
|
47692
|
+
"createdAt"
|
|
47693
|
+
].join(" ");
|
|
47694
|
+
function attestationTsvRow(attestation) {
|
|
47695
|
+
return [
|
|
47696
|
+
attestation.attestationId,
|
|
47697
|
+
tsvSafe(sanitizeDisplay(attestation.documentId)),
|
|
47698
|
+
tsvSafe(sanitizeDisplay(attestation.version)),
|
|
47699
|
+
tsvSafe(sanitizeDisplay(attestation.classification)),
|
|
47700
|
+
attestation.issuedDate,
|
|
47701
|
+
String(attestation.totalFindings),
|
|
47702
|
+
String(attestation.fileSizeBytes),
|
|
47703
|
+
toIso(attestation.createdAt)
|
|
47704
|
+
].join(" ");
|
|
47705
|
+
}
|
|
47706
|
+
async function fetchAttestations(organizationId, pentestIdentifier) {
|
|
47707
|
+
const result = await authenticatedRequest(
|
|
47708
|
+
(client) => client.pentestAttestation.listCustomerAttestations({
|
|
47709
|
+
params: { organizationId, pentestId: pentestIdentifier }
|
|
47710
|
+
})
|
|
47711
|
+
);
|
|
47712
|
+
if (result.status !== SUCCESS_STATUS) {
|
|
47713
|
+
failDeliverable(result.status, result.body, "attestation", pentestIdentifier, "list");
|
|
47714
|
+
}
|
|
47715
|
+
return sortAttestationsNewestFirst(result.body.attestations);
|
|
47716
|
+
}
|
|
47717
|
+
async function runAttestationList(pentestIdentifier, options) {
|
|
47718
|
+
const output = parseOutput(options.output);
|
|
47719
|
+
requirePentestIdentifier(pentestIdentifier);
|
|
47720
|
+
const organizationId = await resolveOrganizationId();
|
|
47721
|
+
const attestations = await fetchAttestations(organizationId, pentestIdentifier);
|
|
47722
|
+
if (output === "json") {
|
|
47723
|
+
console.log(JSON.stringify(attestations, null, 2));
|
|
47724
|
+
return;
|
|
47725
|
+
}
|
|
47726
|
+
if (output === "tsv") {
|
|
47727
|
+
console.log(ATTESTATION_TSV_HEADER);
|
|
47728
|
+
for (const attestation of attestations) {
|
|
47729
|
+
console.log(attestationTsvRow(attestation));
|
|
47730
|
+
}
|
|
47731
|
+
return;
|
|
47732
|
+
}
|
|
47733
|
+
if (attestations.length === 0) {
|
|
47734
|
+
console.log(`No letters of attestation have been shared for pentest ${pentestIdentifier}.`);
|
|
47735
|
+
return;
|
|
47736
|
+
}
|
|
47737
|
+
const header = `Found ${attestations.length} letter${attestations.length === 1 ? "" : "s"} of attestation (newest first):`;
|
|
47738
|
+
await runInteractivePager({
|
|
47739
|
+
items: attestations,
|
|
47740
|
+
header,
|
|
47741
|
+
renderItem: (attestation) => renderAttestationLines(attestation),
|
|
47742
|
+
disabled: options.pager === false
|
|
47743
|
+
});
|
|
47744
|
+
}
|
|
47745
|
+
function createAttestationListCommand() {
|
|
47746
|
+
return new Command("list").description("List the letters of attestation shared for a pentest").argument("<pentestId>", "Pentest display ID (e.g. PT-3) or UUID").option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").option("--no-pager", "Disable the interactive arrow-key pager for table output").addHelpText(
|
|
47747
|
+
"after",
|
|
47748
|
+
`
|
|
47749
|
+
Works with a browser session from \`tracecli auth login\` or a Trace API key
|
|
47750
|
+
(TRACE_API_KEY).
|
|
47751
|
+
|
|
47752
|
+
Examples:
|
|
47753
|
+
tracecli pentest attestation list PT-3
|
|
47754
|
+
tracecli pentest attestation list PT-3 --output json | jq '.[0].attestationId'
|
|
47755
|
+
tracecli pentest attestation list PT-3 --output tsv | cut -f1,2`
|
|
47756
|
+
).action(async (pentestId, options) => {
|
|
47757
|
+
await runAttestationList(pentestId, options);
|
|
47758
|
+
});
|
|
47759
|
+
}
|
|
47760
|
+
async function runAttestationDownload(pentestIdentifier, options) {
|
|
47761
|
+
const output = parseOutput(options.output);
|
|
47762
|
+
requirePentestIdentifier(pentestIdentifier);
|
|
47763
|
+
if (options.attestation !== void 0 && !isUuid(options.attestation)) {
|
|
47764
|
+
console.error(
|
|
47765
|
+
colorError(
|
|
47766
|
+
`Invalid --attestation '${options.attestation}'. Pass the UUID shown by \`tracecli pentest attestation list ${pentestIdentifier}\`.`
|
|
47767
|
+
)
|
|
47768
|
+
);
|
|
47769
|
+
process.exit(1);
|
|
47770
|
+
}
|
|
47771
|
+
const organizationId = await resolveOrganizationId();
|
|
47772
|
+
let attestationId = options.attestation;
|
|
47773
|
+
if (attestationId === void 0) {
|
|
47774
|
+
const attestations = await fetchAttestations(organizationId, pentestIdentifier);
|
|
47775
|
+
const newest = attestations[0];
|
|
47776
|
+
if (newest === void 0) {
|
|
47777
|
+
console.error(
|
|
47778
|
+
colorError(
|
|
47779
|
+
`No letters of attestation have been shared for pentest ${pentestIdentifier} yet.`
|
|
47780
|
+
)
|
|
47781
|
+
);
|
|
47782
|
+
process.exit(1);
|
|
47783
|
+
}
|
|
47784
|
+
if (attestations.length > 1 && output === "table") {
|
|
47785
|
+
console.error(
|
|
47786
|
+
`Downloading the newest of ${attestations.length} letters (${sanitizeDisplay(newest.documentId)}). Pass --attestation <id> for another.`
|
|
47787
|
+
);
|
|
47788
|
+
}
|
|
47789
|
+
attestationId = newest.attestationId;
|
|
47790
|
+
}
|
|
47791
|
+
const result = await authenticatedRequest(
|
|
47792
|
+
(client) => client.pentestAttestation.downloadCustomerAttestation({
|
|
47793
|
+
params: { organizationId, pentestId: pentestIdentifier, attestationId },
|
|
47794
|
+
query: { disposition: "attachment" }
|
|
47795
|
+
})
|
|
47796
|
+
);
|
|
47797
|
+
if (result.status !== SUCCESS_STATUS) {
|
|
47798
|
+
failDeliverable(result.status, result.body, "attestation", pentestIdentifier, "download");
|
|
47799
|
+
}
|
|
47800
|
+
const { attestation, downloadUrl } = result.body;
|
|
47801
|
+
const fileName = attestationFileName(attestation);
|
|
47802
|
+
const { destination, bytes } = await saveDeliverable(downloadUrl, fileName, {
|
|
47803
|
+
...options.out !== void 0 ? { out: options.out } : {},
|
|
47804
|
+
...options.force !== void 0 ? { force: options.force } : {},
|
|
47805
|
+
output
|
|
47806
|
+
});
|
|
47807
|
+
if (output === "json") {
|
|
47808
|
+
console.log(JSON.stringify({ attestation, path: destination, sizeBytes: bytes }, null, 2));
|
|
47809
|
+
return;
|
|
47810
|
+
}
|
|
47811
|
+
if (output === "tsv") {
|
|
47812
|
+
console.log(["attestationId", "documentId", "version", "path", "sizeBytes"].join(" "));
|
|
47813
|
+
console.log(
|
|
47814
|
+
[
|
|
47815
|
+
attestation.attestationId,
|
|
47816
|
+
tsvSafe(sanitizeDisplay(attestation.documentId)),
|
|
47817
|
+
tsvSafe(sanitizeDisplay(attestation.version)),
|
|
47818
|
+
tsvSafe(destination),
|
|
47819
|
+
String(bytes)
|
|
47820
|
+
].join(" ")
|
|
47821
|
+
);
|
|
47822
|
+
return;
|
|
47823
|
+
}
|
|
47824
|
+
console.log(
|
|
47825
|
+
colorSuccess(
|
|
47826
|
+
`Saved ${sanitizeDisplay(attestation.documentId)} (${sanitizeDisplay(attestation.version)}) to ${destination} (${formatArchiveBytes(bytes)}).`
|
|
47827
|
+
)
|
|
47828
|
+
);
|
|
47829
|
+
}
|
|
47830
|
+
function createAttestationDownloadCommand() {
|
|
47831
|
+
return new Command("download").description("Download a letter of attestation (newest by default)").argument("<pentestId>", "Pentest display ID (e.g. PT-3) or UUID").option(
|
|
47832
|
+
"--attestation <id>",
|
|
47833
|
+
"Attestation UUID from `pentest attestation list` (default: the newest letter)"
|
|
47834
|
+
).option("--out <path>", "File to write, or an existing directory (default: working directory)").option("-f, --force", "Overwrite an existing file").option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").addHelpText(
|
|
47835
|
+
"after",
|
|
47836
|
+
`
|
|
47837
|
+
The file is named after the letter's document ID (for example
|
|
47838
|
+
TR-LOA-2026-03-ACME-1A2B3C.pdf). Works with a browser session from \`tracecli auth
|
|
47839
|
+
login\` or a Trace API key (TRACE_API_KEY), so it can run unattended in CI.
|
|
47840
|
+
|
|
47841
|
+
Examples:
|
|
47842
|
+
tracecli pentest attestation download PT-3
|
|
47843
|
+
tracecli pentest attestation download PT-3 --out ./compliance/
|
|
47844
|
+
tracecli pentest attestation download PT-3 --attestation 6f1c... --out ./loa.pdf --force
|
|
47845
|
+
tracecli pentest attestation download PT-3 --output json | jq '.path'`
|
|
47846
|
+
).action(async (pentestId, options) => {
|
|
47847
|
+
await runAttestationDownload(pentestId, options);
|
|
47848
|
+
});
|
|
47849
|
+
}
|
|
47850
|
+
function createAttestationCommand() {
|
|
47851
|
+
const attestation = new Command("attestation").alias("loa").description("List and download letters of attestation");
|
|
47852
|
+
attestation.addCommand(createAttestationListCommand());
|
|
47853
|
+
attestation.addCommand(createAttestationDownloadCommand());
|
|
47854
|
+
return attestation;
|
|
47855
|
+
}
|
|
47856
|
+
|
|
47857
|
+
// src/pentest/evidence.ts
|
|
47858
|
+
import { readFileSync as readFileSync6, statSync as statSync4 } from "fs";
|
|
47859
|
+
import { basename as basename4 } from "path";
|
|
47860
|
+
var SECTION_LIST = ValidationEvidenceSectionValues.join(", ");
|
|
47861
|
+
var KIND_LIST = PentestExploitArtifactKindValues.join(", ");
|
|
47862
|
+
function isSection(value) {
|
|
47863
|
+
return ValidationEvidenceSectionValues.includes(value);
|
|
47864
|
+
}
|
|
47865
|
+
function isKind(value) {
|
|
47866
|
+
return PentestExploitArtifactKindValues.includes(value);
|
|
47867
|
+
}
|
|
47868
|
+
function formatBytes2(bytes) {
|
|
47869
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
47870
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
|
|
47871
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
47872
|
+
}
|
|
47873
|
+
async function resolveFindingId(organizationId, pentestId, identifier) {
|
|
47874
|
+
if (isUuid(identifier)) {
|
|
47875
|
+
return identifier;
|
|
47876
|
+
}
|
|
47877
|
+
const numericMatch = identifier.match(/^(?:PTF-)?(\d+)$/i);
|
|
47878
|
+
if (!numericMatch?.[1]) {
|
|
47879
|
+
console.error(
|
|
47880
|
+
colorError(
|
|
47881
|
+
`Invalid finding identifier '${sanitizeDisplay(identifier)}'. Use a finding number (e.g. PTF-7) or UUID.`
|
|
47882
|
+
)
|
|
47883
|
+
);
|
|
47884
|
+
process.exit(1);
|
|
47885
|
+
}
|
|
47886
|
+
const displayId = Number.parseInt(numericMatch[1], 10);
|
|
47887
|
+
const result = await authenticatedRequest(
|
|
47888
|
+
(client) => client.pentestFinding.listFindings({ params: { organizationId, pentestId } })
|
|
47889
|
+
);
|
|
47890
|
+
if (result.status === UNAUTHORIZED_STATUS2) {
|
|
47891
|
+
exitAuthFailed();
|
|
47892
|
+
}
|
|
47893
|
+
if (result.status !== 200) {
|
|
47894
|
+
console.error(colorError(`Failed to list findings (status ${result.status}).`));
|
|
47895
|
+
process.exit(1);
|
|
47896
|
+
}
|
|
47897
|
+
const match = result.body.find((f) => f.displayId === displayId);
|
|
47898
|
+
if (!match || !("pentestFindingId" in match)) {
|
|
47899
|
+
console.error(
|
|
47900
|
+
colorError(
|
|
47901
|
+
`No finding PTF-${displayId} in this pentest. Unpublished findings have no number yet \u2014 pass the finding UUID instead (\`tracecli pentest findings list --pentest <id> --output json\`).`
|
|
47902
|
+
)
|
|
47903
|
+
);
|
|
47904
|
+
process.exit(1);
|
|
47905
|
+
}
|
|
47906
|
+
return match.pentestFindingId;
|
|
47907
|
+
}
|
|
47908
|
+
async function runAttachEvidence(options) {
|
|
47909
|
+
if (options.section !== void 0 && !isSection(options.section)) {
|
|
47910
|
+
console.error(colorError(`Invalid --section '${sanitizeDisplay(options.section)}'.`));
|
|
47911
|
+
console.error(`Valid sections: ${SECTION_LIST}`);
|
|
47912
|
+
process.exit(1);
|
|
47913
|
+
}
|
|
47914
|
+
if (options.kind !== void 0 && !isKind(options.kind)) {
|
|
47915
|
+
console.error(colorError(`Invalid --kind '${sanitizeDisplay(options.kind)}'.`));
|
|
47916
|
+
console.error(`Valid kinds: ${KIND_LIST}`);
|
|
47917
|
+
process.exit(1);
|
|
47918
|
+
}
|
|
47919
|
+
let sizeBytes;
|
|
47920
|
+
try {
|
|
47921
|
+
const stats = statSync4(options.file);
|
|
47922
|
+
if (!stats.isFile()) {
|
|
47923
|
+
console.error(colorError(`--file '${sanitizeDisplay(options.file)}' is not a regular file.`));
|
|
47924
|
+
process.exit(1);
|
|
47925
|
+
}
|
|
47926
|
+
sizeBytes = stats.size;
|
|
47927
|
+
} catch {
|
|
47928
|
+
console.error(colorError(`Cannot read --file '${sanitizeDisplay(options.file)}'.`));
|
|
47929
|
+
process.exit(1);
|
|
47930
|
+
}
|
|
47931
|
+
if (sizeBytes === 0) {
|
|
47932
|
+
console.error(colorError(`--file '${sanitizeDisplay(options.file)}' is empty.`));
|
|
47933
|
+
process.exit(1);
|
|
47934
|
+
}
|
|
47935
|
+
if (sizeBytes > MAX_STAFF_EVIDENCE_UPLOAD_BYTES) {
|
|
47936
|
+
console.error(
|
|
47937
|
+
colorError(
|
|
47938
|
+
`File is ${formatBytes2(sizeBytes)}; the limit for one exhibit is ${formatBytes2(MAX_STAFF_EVIDENCE_UPLOAD_BYTES)}.`
|
|
47939
|
+
)
|
|
47940
|
+
);
|
|
47941
|
+
process.exit(1);
|
|
47942
|
+
}
|
|
47943
|
+
const organizationId = await resolveOrganizationId();
|
|
47944
|
+
const pentestId = await resolvePentestId(organizationId, options.pentest);
|
|
47945
|
+
const pentestFindingId = await resolveFindingId(organizationId, pentestId, options.finding);
|
|
47946
|
+
const params = { organizationId, pentestId, pentestFindingId };
|
|
47947
|
+
const filename = basename4(options.file);
|
|
47948
|
+
const slot = await authenticatedRequest(
|
|
47949
|
+
(client) => client.pentestFinding.createFindingEvidenceUpload({
|
|
47950
|
+
params,
|
|
47951
|
+
body: { filename, sizeBytes }
|
|
47952
|
+
})
|
|
47953
|
+
);
|
|
47954
|
+
if (slot.status === UNAUTHORIZED_STATUS2) {
|
|
47955
|
+
exitAuthFailed();
|
|
47956
|
+
}
|
|
47957
|
+
if (slot.status !== 201) {
|
|
47958
|
+
const { message } = readErrorFields(slot.body);
|
|
47959
|
+
console.error(
|
|
47960
|
+
colorError(message ?? `Failed to start the evidence upload (status ${slot.status}).`)
|
|
47961
|
+
);
|
|
47962
|
+
process.exit(1);
|
|
47963
|
+
}
|
|
47964
|
+
const body = readFileSync6(options.file);
|
|
47965
|
+
const put = await fetch(slot.body.uploadUrl, { method: "PUT", body: new Uint8Array(body) });
|
|
47966
|
+
if (!put.ok) {
|
|
47967
|
+
console.error(
|
|
47968
|
+
colorError(
|
|
47969
|
+
`Upload to storage failed (status ${put.status}). The upload link is short-lived \u2014 re-run the command.`
|
|
47970
|
+
)
|
|
46958
47971
|
);
|
|
46959
47972
|
process.exit(1);
|
|
46960
47973
|
}
|
|
46961
|
-
const
|
|
46962
|
-
|
|
46963
|
-
const params = {
|
|
46964
|
-
organizationId,
|
|
46965
|
-
pentestId: options.pentest,
|
|
46966
|
-
pentestFindingId: options.finding
|
|
46967
|
-
};
|
|
46968
|
-
const result = viaApiKey ? await authenticatedRequest(
|
|
46969
|
-
(client) => client.pentestFinding.automationStatusUpdate({
|
|
46970
|
-
params,
|
|
46971
|
-
body: { status: target, ...note !== void 0 ? { note } : {} }
|
|
46972
|
-
})
|
|
46973
|
-
) : target === "IN_PROGRESS" ? await authenticatedRequest(
|
|
46974
|
-
(client) => client.pentestFinding.startProgress({ params, body: {} })
|
|
46975
|
-
) : await authenticatedRequest(
|
|
46976
|
-
(client) => client.pentestFinding.reportRemediation({
|
|
47974
|
+
const attached = await authenticatedRequest(
|
|
47975
|
+
(client) => client.pentestFinding.attachFindingEvidence({
|
|
46977
47976
|
params,
|
|
46978
|
-
body:
|
|
47977
|
+
body: {
|
|
47978
|
+
key: slot.body.key,
|
|
47979
|
+
...options.caption !== void 0 ? { caption: options.caption } : {},
|
|
47980
|
+
...options.section !== void 0 && isSection(options.section) ? { section: options.section } : {},
|
|
47981
|
+
...options.kind !== void 0 && isKind(options.kind) ? { kind: options.kind } : {},
|
|
47982
|
+
...options.customerFacing ? { customerFacing: true } : {}
|
|
47983
|
+
}
|
|
46979
47984
|
})
|
|
46980
47985
|
);
|
|
46981
|
-
if (
|
|
46982
|
-
|
|
47986
|
+
if (attached.status === UNAUTHORIZED_STATUS2) {
|
|
47987
|
+
exitAuthFailed();
|
|
47988
|
+
}
|
|
47989
|
+
if (attached.status !== 201) {
|
|
47990
|
+
const { message } = readErrorFields(attached.body);
|
|
47991
|
+
console.error(
|
|
47992
|
+
colorError(message ?? `Failed to attach the evidence (status ${attached.status}).`)
|
|
47993
|
+
);
|
|
47994
|
+
process.exit(1);
|
|
46983
47995
|
}
|
|
46984
47996
|
if (options.output === "json") {
|
|
46985
|
-
console.log(JSON.stringify(
|
|
47997
|
+
console.log(JSON.stringify(attached.body, null, 2));
|
|
46986
47998
|
return;
|
|
46987
47999
|
}
|
|
46988
|
-
const
|
|
46989
|
-
const
|
|
46990
|
-
console.log(
|
|
48000
|
+
const visibility = attached.body.customerFacing ? "customer-facing" : "staff-internal";
|
|
48001
|
+
const placement = attached.body.section ? ` \xB7 ${attached.body.section}` : "";
|
|
48002
|
+
console.log(
|
|
48003
|
+
colorSuccess(
|
|
48004
|
+
`Attached ${attached.body.kind} \xB7 ${formatBytes2(attached.body.sizeBytes)} \xB7 ${visibility}${placement}`
|
|
48005
|
+
)
|
|
48006
|
+
);
|
|
48007
|
+
console.log(` artifact ${attached.body.artifactId}`);
|
|
48008
|
+
if (attached.body.kind === "HAR") {
|
|
48009
|
+
console.log(" HAR captures stay internal \u2014 they can carry a live session token.");
|
|
48010
|
+
} else if (!attached.body.customerFacing) {
|
|
48011
|
+
console.log(" Publish it from the finding\u2019s evidence panel in the staff console.");
|
|
48012
|
+
}
|
|
46991
48013
|
}
|
|
46992
|
-
function
|
|
46993
|
-
return new Command("
|
|
46994
|
-
"--status <status>",
|
|
46995
|
-
"IN_PROGRESS (start work) or REMEDIATED (ready for retest)"
|
|
46996
|
-
).option("--note <text>", "Optional context when reporting remediation").option("-o, --output <format>", "Output format: text, json (default: text)", "text").addHelpText(
|
|
48014
|
+
function createAttachEvidenceCommand() {
|
|
48015
|
+
return new Command("attach-evidence").description("Attach a screenshot, recording, or capture to a pentest finding (staff)").requiredOption("--pentest <id>", "Pentest display ID (e.g. PT-3) or UUID").requiredOption("--finding <id>", "Finding number (e.g. PTF-7) or UUID").requiredOption("--file <path>", "Path to the evidence file").option("--caption <text>", "What the exhibit shows; rendered as its label").option("--section <section>", `Report section it backs (${SECTION_LIST})`).option("--kind <kind>", `Override the derived exhibit kind (${KIND_LIST})`).option("--customer-facing", "Publish to the customer (not allowed for HAR or video)").option("-o, --output <format>", "Output format: text, json (default: text)", "text").addHelpText(
|
|
46997
48016
|
"after",
|
|
46998
48017
|
`
|
|
46999
|
-
|
|
47000
|
-
|
|
47001
|
-
|
|
47002
|
-
sending REMEDIATED) keeps it out of the retest queue.
|
|
47003
|
-
|
|
47004
|
-
IN_PROGRESS is allowed from OPEN or VERIFIED_OPEN. REMEDIATED is allowed from
|
|
47005
|
-
OPEN, IN_PROGRESS, or VERIFIED_OPEN.
|
|
47006
|
-
|
|
47007
|
-
A signed-in session (\`tracecli auth login\`) attributes the change to you. A
|
|
47008
|
-
Trace API key works too, once Trace enables finding automation for your
|
|
47009
|
-
organization: those writes are recorded as \`api-key:<key name>\` and show up as
|
|
47010
|
-
yours in the finding's history.
|
|
48018
|
+
The file uploads straight to storage, so recordings of any reasonable size work.
|
|
48019
|
+
Videos and HAR captures always land staff-internal \u2014 publish a video with the
|
|
48020
|
+
toggle in the staff console's evidence panel after reviewing it for redaction.
|
|
47011
48021
|
|
|
47012
48022
|
Examples:
|
|
47013
|
-
tracecli pentest findings
|
|
47014
|
-
tracecli pentest findings
|
|
47015
|
-
|
|
47016
|
-
|
|
48023
|
+
tracecli pentest findings attach-evidence --pentest PT-3 --finding 7 --file ./poc.png
|
|
48024
|
+
tracecli pentest findings attach-evidence --pentest PT-3 --finding PTF-7 --file ./poc.mov \\
|
|
48025
|
+
--caption "Cookie survives logout, replayed 40s later" --section proofOfConcept
|
|
48026
|
+
tracecli pentest findings attach-evidence --pentest PT-3 --finding 7 --file ./roster.json \\
|
|
48027
|
+
--caption "Full member roster returned to a non-member" --customer-facing --output json`
|
|
47017
48028
|
).action(async (options) => {
|
|
47018
|
-
await
|
|
48029
|
+
await runAttachEvidence(options);
|
|
47019
48030
|
});
|
|
47020
48031
|
}
|
|
47021
48032
|
|
|
@@ -48130,12 +49141,14 @@ function createPentestCommand() {
|
|
|
48130
49141
|
pentest.addCommand(createUpdateCommand4());
|
|
48131
49142
|
pentest.addCommand(createQuestionnaireCommand());
|
|
48132
49143
|
pentest.addCommand(createFindingsCommand());
|
|
49144
|
+
pentest.addCommand(createReportCommand());
|
|
49145
|
+
pentest.addCommand(createAttestationCommand());
|
|
48133
49146
|
return pentest;
|
|
48134
49147
|
}
|
|
48135
49148
|
|
|
48136
49149
|
// src/update/commands.ts
|
|
48137
49150
|
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
|
|
48138
|
-
import { dirname as dirname3, join as
|
|
49151
|
+
import { dirname as dirname3, join as join7 } from "path";
|
|
48139
49152
|
import { createInterface as createInterface4 } from "readline/promises";
|
|
48140
49153
|
var UPDATE_CACHE_FILE = "update-check.json";
|
|
48141
49154
|
var UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -48144,7 +49157,7 @@ var DISABLE_UPDATE_NOTIFIER_ENV = "TRACECLI_DISABLE_UPDATE_NOTIFIER";
|
|
|
48144
49157
|
var REGISTRY_URL_ENV = "TRACECLI_NPM_REGISTRY_URL";
|
|
48145
49158
|
var noticePrinted = false;
|
|
48146
49159
|
function getUpdateCheckCachePath() {
|
|
48147
|
-
return
|
|
49160
|
+
return join7(getTraceDir(), UPDATE_CACHE_FILE);
|
|
48148
49161
|
}
|
|
48149
49162
|
function readUpdateCheckCache() {
|
|
48150
49163
|
const cachePath = getUpdateCheckCachePath();
|
|
@@ -48518,153 +49531,8 @@ Examples:
|
|
|
48518
49531
|
|
|
48519
49532
|
// src/upload/download.ts
|
|
48520
49533
|
import { createHash as createHash2 } from "crypto";
|
|
48521
|
-
import { open as
|
|
48522
|
-
import { basename as
|
|
48523
|
-
|
|
48524
|
-
// src/upload/prepare.ts
|
|
48525
|
-
import { createHash } from "crypto";
|
|
48526
|
-
import { createReadStream, statSync as statSync4 } from "fs";
|
|
48527
|
-
import { open } from "fs/promises";
|
|
48528
|
-
import { basename as basename3 } from "path";
|
|
48529
|
-
var CodeUploadPrepareError = class extends Error {
|
|
48530
|
-
constructor(message) {
|
|
48531
|
-
super(message);
|
|
48532
|
-
this.name = "CodeUploadPrepareError";
|
|
48533
|
-
}
|
|
48534
|
-
};
|
|
48535
|
-
var KIND_BY_INPUT = {
|
|
48536
|
-
source: "SOURCE_ARCHIVE",
|
|
48537
|
-
code: "SOURCE_ARCHIVE",
|
|
48538
|
-
"source-archive": "SOURCE_ARCHIVE",
|
|
48539
|
-
source_archive: "SOURCE_ARCHIVE",
|
|
48540
|
-
SOURCE_ARCHIVE: "SOURCE_ARCHIVE",
|
|
48541
|
-
apk: "ANDROID_APK",
|
|
48542
|
-
android: "ANDROID_APK",
|
|
48543
|
-
"android-apk": "ANDROID_APK",
|
|
48544
|
-
android_apk: "ANDROID_APK",
|
|
48545
|
-
ANDROID_APK: "ANDROID_APK",
|
|
48546
|
-
ipa: "IOS_IPA",
|
|
48547
|
-
ios: "IOS_IPA",
|
|
48548
|
-
"ios-ipa": "IOS_IPA",
|
|
48549
|
-
ios_ipa: "IOS_IPA",
|
|
48550
|
-
IOS_IPA: "IOS_IPA"
|
|
48551
|
-
};
|
|
48552
|
-
function normalizeArtifactKind(input) {
|
|
48553
|
-
const trimmed = input.trim();
|
|
48554
|
-
return KIND_BY_INPUT[trimmed] ?? KIND_BY_INPUT[trimmed.toLowerCase()] ?? null;
|
|
48555
|
-
}
|
|
48556
|
-
function inferArtifactKindFromFilename(filename) {
|
|
48557
|
-
const lower = filename.toLowerCase();
|
|
48558
|
-
if (lower.endsWith(".zip")) return "SOURCE_ARCHIVE";
|
|
48559
|
-
if (lower.endsWith(".apk")) return "ANDROID_APK";
|
|
48560
|
-
if (lower.endsWith(".ipa")) return "IOS_IPA";
|
|
48561
|
-
return null;
|
|
48562
|
-
}
|
|
48563
|
-
function requiredExtensionForKind(kind) {
|
|
48564
|
-
if (kind === "ANDROID_APK") return ".apk";
|
|
48565
|
-
if (kind === "IOS_IPA") return ".ipa";
|
|
48566
|
-
return ".zip";
|
|
48567
|
-
}
|
|
48568
|
-
function defaultDisplayName(originalFilename) {
|
|
48569
|
-
const base = originalFilename.replace(/\.(zip|apk|ipa)$/i, "").trim();
|
|
48570
|
-
return base.slice(0, 200) || "source";
|
|
48571
|
-
}
|
|
48572
|
-
function formatArchiveBytes(bytes) {
|
|
48573
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
48574
|
-
if (bytes < 1024 * 1024) {
|
|
48575
|
-
const kb = bytes / 1024;
|
|
48576
|
-
return `${kb.toFixed(Number.isInteger(kb) ? 0 : 1)} KB`;
|
|
48577
|
-
}
|
|
48578
|
-
if (bytes < 1024 * 1024 * 1024) {
|
|
48579
|
-
const mb = bytes / (1024 * 1024);
|
|
48580
|
-
return `${mb.toFixed(Number.isInteger(mb) ? 0 : 1)} MB`;
|
|
48581
|
-
}
|
|
48582
|
-
const gb = bytes / (1024 * 1024 * 1024);
|
|
48583
|
-
return `${gb.toFixed(Number.isInteger(gb) ? 0 : 1)} GB`;
|
|
48584
|
-
}
|
|
48585
|
-
function archiveTooLargeMessage() {
|
|
48586
|
-
return `The file must be ${formatArchiveBytes(CODE_UPLOAD_MAX_BYTES)} or smaller.`;
|
|
48587
|
-
}
|
|
48588
|
-
async function hashFileSha256(filePath) {
|
|
48589
|
-
const hash2 = createHash("sha256");
|
|
48590
|
-
const stream = createReadStream(filePath);
|
|
48591
|
-
for await (const chunk of stream) {
|
|
48592
|
-
hash2.update(chunk);
|
|
48593
|
-
}
|
|
48594
|
-
return hash2.digest("hex");
|
|
48595
|
-
}
|
|
48596
|
-
async function readFileSlice(filePath, start, endExclusive) {
|
|
48597
|
-
const length = endExclusive - start;
|
|
48598
|
-
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(length) || start < 0 || length < 1) {
|
|
48599
|
-
throw new Error(
|
|
48600
|
-
`Invalid file slice: start=${String(start)} endExclusive=${String(endExclusive)}.`
|
|
48601
|
-
);
|
|
48602
|
-
}
|
|
48603
|
-
const buffer = Buffer.alloc(length);
|
|
48604
|
-
const handle = await open(filePath, "r");
|
|
48605
|
-
try {
|
|
48606
|
-
const { bytesRead } = await handle.read(buffer, 0, length, start);
|
|
48607
|
-
if (bytesRead !== length) {
|
|
48608
|
-
throw new Error(
|
|
48609
|
-
`Expected to read ${String(length)} bytes at offset ${String(start)}, got ${String(bytesRead)}.`
|
|
48610
|
-
);
|
|
48611
|
-
}
|
|
48612
|
-
return buffer;
|
|
48613
|
-
} finally {
|
|
48614
|
-
await handle.close();
|
|
48615
|
-
}
|
|
48616
|
-
}
|
|
48617
|
-
function assertPreparedMatchesUpload(upload, prepared) {
|
|
48618
|
-
if (upload.sizeBytes !== prepared.sizeBytes || upload.archiveSha256 !== prepared.archiveSha256) {
|
|
48619
|
-
throw new CodeUploadPrepareError(
|
|
48620
|
-
"The file does not match the interrupted upload (size or SHA-256 differs)."
|
|
48621
|
-
);
|
|
48622
|
-
}
|
|
48623
|
-
}
|
|
48624
|
-
async function prepareUploadFile(filePath, options = {}) {
|
|
48625
|
-
let sizeBytes;
|
|
48626
|
-
try {
|
|
48627
|
-
const stats = statSync4(filePath);
|
|
48628
|
-
if (!stats.isFile()) {
|
|
48629
|
-
throw new CodeUploadPrepareError("Path is not a regular file. Pass a .zip, .apk, or .ipa.");
|
|
48630
|
-
}
|
|
48631
|
-
sizeBytes = stats.size;
|
|
48632
|
-
} catch (error48) {
|
|
48633
|
-
if (error48 instanceof CodeUploadPrepareError) throw error48;
|
|
48634
|
-
throw new CodeUploadPrepareError("Cannot read that file.");
|
|
48635
|
-
}
|
|
48636
|
-
if (!Number.isSafeInteger(sizeBytes) || sizeBytes < 1) {
|
|
48637
|
-
throw new CodeUploadPrepareError("The file is empty.");
|
|
48638
|
-
}
|
|
48639
|
-
if (sizeBytes > CODE_UPLOAD_MAX_BYTES) {
|
|
48640
|
-
throw new CodeUploadPrepareError(archiveTooLargeMessage());
|
|
48641
|
-
}
|
|
48642
|
-
const originalFilename = basename3(filePath);
|
|
48643
|
-
const inferred = inferArtifactKindFromFilename(originalFilename);
|
|
48644
|
-
const artifactKind = options.artifactKind ?? inferred;
|
|
48645
|
-
if (artifactKind == null) {
|
|
48646
|
-
throw new CodeUploadPrepareError("Pass a .zip source archive, an .apk, or an .ipa.");
|
|
48647
|
-
}
|
|
48648
|
-
const expected = requiredExtensionForKind(artifactKind);
|
|
48649
|
-
if (!originalFilename.toLowerCase().endsWith(expected)) {
|
|
48650
|
-
const label = isApplicationBuildArtifactKind(artifactKind) ? artifactKind === "ANDROID_APK" ? "Android APK" : "iOS package" : "source archive";
|
|
48651
|
-
throw new CodeUploadPrepareError(`A ${label} must end in ${expected}.`);
|
|
48652
|
-
}
|
|
48653
|
-
const displayName = (options.displayName ?? defaultDisplayName(originalFilename)).trim();
|
|
48654
|
-
if (displayName.length < 1 || displayName.length > 200) {
|
|
48655
|
-
throw new CodeUploadPrepareError("Display name must be 1 to 200 characters.");
|
|
48656
|
-
}
|
|
48657
|
-
return {
|
|
48658
|
-
path: filePath,
|
|
48659
|
-
displayName,
|
|
48660
|
-
originalFilename,
|
|
48661
|
-
sizeBytes,
|
|
48662
|
-
archiveSha256: await hashFileSha256(filePath),
|
|
48663
|
-
artifactKind
|
|
48664
|
-
};
|
|
48665
|
-
}
|
|
48666
|
-
|
|
48667
|
-
// src/upload/download.ts
|
|
49534
|
+
import { open as open3, rename as rename2, rm as rm2, stat as stat2 } from "fs/promises";
|
|
49535
|
+
import { basename as basename5, join as join8, resolve as resolve2 } from "path";
|
|
48668
49536
|
var CodeUploadDownloadError = class extends Error {
|
|
48669
49537
|
constructor(message) {
|
|
48670
49538
|
super(message);
|
|
@@ -48674,7 +49542,7 @@ var CodeUploadDownloadError = class extends Error {
|
|
|
48674
49542
|
function safeDownloadFilename(upload) {
|
|
48675
49543
|
const extension = requiredExtensionForKind(upload.artifactKind);
|
|
48676
49544
|
const fallback = `app${extension}`;
|
|
48677
|
-
const stripped = Array.from(
|
|
49545
|
+
const stripped = Array.from(basename5(upload.originalFilename.replace(/\\/g, "/")), (char) => {
|
|
48678
49546
|
const code = char.codePointAt(0) ?? 0;
|
|
48679
49547
|
return code <= 31 || code === 127 ? "_" : char;
|
|
48680
49548
|
}).join("").trim();
|
|
@@ -48683,18 +49551,18 @@ function safeDownloadFilename(upload) {
|
|
|
48683
49551
|
}
|
|
48684
49552
|
async function resolveDownloadDestination(upload, outOption) {
|
|
48685
49553
|
const filename = safeDownloadFilename(upload);
|
|
48686
|
-
if (outOption === void 0) return
|
|
48687
|
-
const target =
|
|
49554
|
+
if (outOption === void 0) return resolve2(process.cwd(), filename);
|
|
49555
|
+
const target = resolve2(outOption);
|
|
48688
49556
|
try {
|
|
48689
|
-
const info = await
|
|
48690
|
-
if (info.isDirectory()) return
|
|
49557
|
+
const info = await stat2(target);
|
|
49558
|
+
if (info.isDirectory()) return join8(target, filename);
|
|
48691
49559
|
} catch {
|
|
48692
49560
|
}
|
|
48693
49561
|
return target;
|
|
48694
49562
|
}
|
|
48695
|
-
async function
|
|
49563
|
+
async function pathExists2(path) {
|
|
48696
49564
|
try {
|
|
48697
|
-
await
|
|
49565
|
+
await stat2(path);
|
|
48698
49566
|
return true;
|
|
48699
49567
|
} catch {
|
|
48700
49568
|
return false;
|
|
@@ -48726,8 +49594,8 @@ async function downloadCodeUploadToFile(upload, downloadUrl, destination, option
|
|
|
48726
49594
|
);
|
|
48727
49595
|
}
|
|
48728
49596
|
}
|
|
48729
|
-
await
|
|
48730
|
-
const file2 = await
|
|
49597
|
+
await rm2(partialPath, { force: true });
|
|
49598
|
+
const file2 = await open3(partialPath, "wx");
|
|
48731
49599
|
const hash2 = createHash2("sha256");
|
|
48732
49600
|
const reader = response.body.getReader();
|
|
48733
49601
|
let downloadedBytes = 0;
|
|
@@ -48744,7 +49612,7 @@ async function downloadCodeUploadToFile(upload, downloadUrl, destination, option
|
|
|
48744
49612
|
);
|
|
48745
49613
|
}
|
|
48746
49614
|
hash2.update(chunk.value);
|
|
48747
|
-
await
|
|
49615
|
+
await writeAll2(file2, chunk.value);
|
|
48748
49616
|
options.onProgress?.(progress(downloadedBytes, upload.sizeBytes));
|
|
48749
49617
|
}
|
|
48750
49618
|
await file2.close();
|
|
@@ -48759,10 +49627,10 @@ async function downloadCodeUploadToFile(upload, downloadUrl, destination, option
|
|
|
48759
49627
|
"Downloaded bytes do not match the SHA-256 recorded at upload time."
|
|
48760
49628
|
);
|
|
48761
49629
|
}
|
|
48762
|
-
await
|
|
49630
|
+
await rename2(partialPath, destination);
|
|
48763
49631
|
} catch (error48) {
|
|
48764
49632
|
await file2.close().catch(() => void 0);
|
|
48765
|
-
await
|
|
49633
|
+
await rm2(partialPath, { force: true });
|
|
48766
49634
|
if (error48 instanceof CodeUploadDownloadError) throw error48;
|
|
48767
49635
|
const detail = error48 instanceof Error ? error48.message : "unknown error";
|
|
48768
49636
|
throw new CodeUploadDownloadError(`Download from storage failed: ${detail}.`);
|
|
@@ -48775,7 +49643,7 @@ function progress(downloadedBytes, totalBytes) {
|
|
|
48775
49643
|
fraction: totalBytes === 0 ? 1 : downloadedBytes / totalBytes
|
|
48776
49644
|
};
|
|
48777
49645
|
}
|
|
48778
|
-
async function
|
|
49646
|
+
async function writeAll2(file2, bytes) {
|
|
48779
49647
|
let offset = 0;
|
|
48780
49648
|
while (offset < bytes.byteLength) {
|
|
48781
49649
|
const { bytesWritten } = await file2.write(bytes, offset, bytes.byteLength - offset);
|
|
@@ -48911,13 +49779,13 @@ function progress2(uploadedBytes, totalBytes) {
|
|
|
48911
49779
|
};
|
|
48912
49780
|
}
|
|
48913
49781
|
function defaultWait(milliseconds) {
|
|
48914
|
-
return new Promise((
|
|
49782
|
+
return new Promise((resolve3) => setTimeout(resolve3, milliseconds));
|
|
48915
49783
|
}
|
|
48916
49784
|
|
|
48917
49785
|
// src/upload/commands.ts
|
|
48918
49786
|
var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["READY", "REJECTED", "FAILED", "DELETING"]);
|
|
48919
49787
|
var DEFAULT_RUNTIME = {
|
|
48920
|
-
wait: (milliseconds) => new Promise((
|
|
49788
|
+
wait: (milliseconds) => new Promise((resolve3) => setTimeout(resolve3, milliseconds)),
|
|
48921
49789
|
pollIntervalMs: 3e3,
|
|
48922
49790
|
fetch: (input, init) => globalThis.fetch(input, init)
|
|
48923
49791
|
};
|
|
@@ -49173,14 +50041,14 @@ function writeProgress(progress3) {
|
|
|
49173
50041
|
`\rUploading ${pct}% (${formatArchiveBytes(progress3.uploadedBytes)} / ${formatArchiveBytes(progress3.totalBytes)})`
|
|
49174
50042
|
);
|
|
49175
50043
|
}
|
|
49176
|
-
function
|
|
50044
|
+
function writeDownloadProgress2(progress3) {
|
|
49177
50045
|
if (!process.stderr.isTTY) return;
|
|
49178
50046
|
const pct = Math.floor(progress3.fraction * 100);
|
|
49179
50047
|
process.stderr.write(
|
|
49180
50048
|
`\rDownloading ${pct}% (${formatArchiveBytes(progress3.downloadedBytes)} / ${formatArchiveBytes(progress3.totalBytes)})`
|
|
49181
50049
|
);
|
|
49182
50050
|
}
|
|
49183
|
-
function
|
|
50051
|
+
function finishProgress2() {
|
|
49184
50052
|
if (process.stderr.isTTY) process.stderr.write("\n");
|
|
49185
50053
|
}
|
|
49186
50054
|
async function listUploads(organizationId, query = {}) {
|
|
@@ -49310,9 +50178,9 @@ async function runTransfer(filePath, options, runtime) {
|
|
|
49310
50178
|
refreshParts: () => refreshRemainingParts(organizationId, codeUploadId)
|
|
49311
50179
|
}
|
|
49312
50180
|
);
|
|
49313
|
-
|
|
50181
|
+
finishProgress2();
|
|
49314
50182
|
} catch (error48) {
|
|
49315
|
-
|
|
50183
|
+
finishProgress2();
|
|
49316
50184
|
const detail = error48 instanceof Error ? error48.message : "Upload to storage failed.";
|
|
49317
50185
|
console.error(colorError(detail));
|
|
49318
50186
|
console.error(resumeHint(codeUploadId, filePath));
|
|
@@ -49425,9 +50293,9 @@ async function runResume(codeUploadIdArg, filePath, options, runtime) {
|
|
|
49425
50293
|
refreshParts: () => refreshRemainingParts(organizationId, codeUploadId)
|
|
49426
50294
|
}
|
|
49427
50295
|
);
|
|
49428
|
-
|
|
50296
|
+
finishProgress2();
|
|
49429
50297
|
} catch (error48) {
|
|
49430
|
-
|
|
50298
|
+
finishProgress2();
|
|
49431
50299
|
const detail = error48 instanceof Error ? error48.message : "Upload to storage failed.";
|
|
49432
50300
|
console.error(colorError(detail));
|
|
49433
50301
|
console.error(resumeHint(codeUploadId, filePath));
|
|
@@ -49505,7 +50373,7 @@ async function runDownload(codeUploadIdArg, options, runtime) {
|
|
|
49505
50373
|
}
|
|
49506
50374
|
const { upload, downloadUrl } = result.body;
|
|
49507
50375
|
const destination = await resolveDownloadDestination(upload, options.out);
|
|
49508
|
-
if (!options.force && await
|
|
50376
|
+
if (!options.force && await pathExists2(destination)) {
|
|
49509
50377
|
console.error(
|
|
49510
50378
|
colorError(`${destination} already exists. Pass --force to overwrite it or --out <path>.`)
|
|
49511
50379
|
);
|
|
@@ -49519,11 +50387,11 @@ async function runDownload(codeUploadIdArg, options, runtime) {
|
|
|
49519
50387
|
try {
|
|
49520
50388
|
await downloadCodeUploadToFile(upload, downloadUrl, destination, {
|
|
49521
50389
|
fetch: runtime.fetch,
|
|
49522
|
-
onProgress:
|
|
50390
|
+
onProgress: writeDownloadProgress2
|
|
49523
50391
|
});
|
|
49524
|
-
|
|
50392
|
+
finishProgress2();
|
|
49525
50393
|
} catch (error48) {
|
|
49526
|
-
|
|
50394
|
+
finishProgress2();
|
|
49527
50395
|
const detail = error48 instanceof Error ? error48.message : "Download from storage failed.";
|
|
49528
50396
|
console.error(colorError(detail));
|
|
49529
50397
|
process.exit(1);
|