securewithtrace 0.44.0 → 0.45.1
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 +1327 -473
- 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,
|
|
@@ -39507,12 +39659,19 @@ var SEVERITY_ORDER = {
|
|
|
39507
39659
|
LOW: 3,
|
|
39508
39660
|
INFORMATIONAL: 4
|
|
39509
39661
|
};
|
|
39662
|
+
function printJson(value) {
|
|
39663
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}
|
|
39664
|
+
`);
|
|
39665
|
+
}
|
|
39510
39666
|
function formatStatus(status) {
|
|
39511
39667
|
return status.toLowerCase().replace(/_/g, " ");
|
|
39512
39668
|
}
|
|
39513
39669
|
function tsvSafe(value) {
|
|
39514
39670
|
return (value ?? "").replace(/[\t\r\n]/g, " ");
|
|
39515
39671
|
}
|
|
39672
|
+
function formulaSafe(value) {
|
|
39673
|
+
return /^[=+\-@]/.test(value) ? `'${value}` : value;
|
|
39674
|
+
}
|
|
39516
39675
|
|
|
39517
39676
|
// src/pager.ts
|
|
39518
39677
|
function isInteractiveEnvironment() {
|
|
@@ -39627,6 +39786,21 @@ function extractRepoName(input) {
|
|
|
39627
39786
|
}
|
|
39628
39787
|
return input;
|
|
39629
39788
|
}
|
|
39789
|
+
function normalizeRepoUrlForMatch(url2) {
|
|
39790
|
+
let normalized = url2.trim().toLowerCase();
|
|
39791
|
+
const scp = normalized.match(/^[a-z0-9._-]+@([^:/]+):(.+)$/);
|
|
39792
|
+
if (scp) {
|
|
39793
|
+
normalized = `${scp[1]}/${scp[2]}`;
|
|
39794
|
+
} else {
|
|
39795
|
+
normalized = normalized.replace(/^[a-z][a-z0-9+.-]*:\/\//, "").replace(/^[a-z0-9._-]+@/, "");
|
|
39796
|
+
}
|
|
39797
|
+
let previous;
|
|
39798
|
+
do {
|
|
39799
|
+
previous = normalized;
|
|
39800
|
+
normalized = normalized.replace(/\/+$/, "").replace(/\.git$/, "");
|
|
39801
|
+
} while (normalized !== previous);
|
|
39802
|
+
return normalized;
|
|
39803
|
+
}
|
|
39630
39804
|
var BRANCH_NAME_PATTERN = /^[a-zA-Z0-9._/+-]+$/;
|
|
39631
39805
|
var TRACE_GIT_ASKPASS_TOKEN_ENV = "TRACE_GIT_ASKPASS_TOKEN";
|
|
39632
39806
|
var DEFAULT_LIMIT = 30;
|
|
@@ -39634,6 +39808,8 @@ var DEFAULT_PAGE = 1;
|
|
|
39634
39808
|
var MAX_LIMIT = 100;
|
|
39635
39809
|
var REPOSITORY_PLATFORMS = ["GITHUB", "GITLAB", "BITBUCKET"];
|
|
39636
39810
|
var REPOSITORY_STATUSES = ["CONNECTED", "DISCONNECTED"];
|
|
39811
|
+
var SCANNING_INCLUSIONS = ["included", "excluded"];
|
|
39812
|
+
var SCANNING_INCLUSION_BATCH_SIZE = 500;
|
|
39637
39813
|
function validateBranchName(branch) {
|
|
39638
39814
|
if (branch.startsWith("-")) {
|
|
39639
39815
|
throw new Error(`Invalid branch name: '${branch}' \u2014 branch names cannot start with a dash`);
|
|
@@ -39687,6 +39863,18 @@ function normalizeStatus(value, source) {
|
|
|
39687
39863
|
`${source} must be one of: ${REPOSITORY_STATUSES.join(", ")}. Received '${value}'.`
|
|
39688
39864
|
);
|
|
39689
39865
|
}
|
|
39866
|
+
function normalizeScanningInclusion(value, source) {
|
|
39867
|
+
const normalized = value.trim().toLowerCase();
|
|
39868
|
+
if (SCANNING_INCLUSIONS.includes(normalized)) {
|
|
39869
|
+
return normalized;
|
|
39870
|
+
}
|
|
39871
|
+
throw new Error(
|
|
39872
|
+
`${source} must be one of: ${SCANNING_INCLUSIONS.join(", ")}. Received '${value}'.`
|
|
39873
|
+
);
|
|
39874
|
+
}
|
|
39875
|
+
function scanningInclusionOf(repo) {
|
|
39876
|
+
return repo.includedInScanning ? "included" : "excluded";
|
|
39877
|
+
}
|
|
39690
39878
|
function normalizeFilterField(rawField) {
|
|
39691
39879
|
const field = rawField.trim().toLowerCase();
|
|
39692
39880
|
switch (field) {
|
|
@@ -39711,9 +39899,14 @@ function normalizeFilterField(rawField) {
|
|
|
39711
39899
|
case "primarylanguage":
|
|
39712
39900
|
case "primary_language":
|
|
39713
39901
|
return "language";
|
|
39902
|
+
case "scanning":
|
|
39903
|
+
case "inclusion":
|
|
39904
|
+
case "included_in_scanning":
|
|
39905
|
+
case "includedinscanning":
|
|
39906
|
+
return "scanning";
|
|
39714
39907
|
default:
|
|
39715
39908
|
throw new Error(
|
|
39716
|
-
`Unknown repository filter field '${rawField}'. Supported fields: platform, status, url, repo, branch, language.`
|
|
39909
|
+
`Unknown repository filter field '${rawField}'. Supported fields: platform, status, url, repo, branch, language, scanning.`
|
|
39717
39910
|
);
|
|
39718
39911
|
}
|
|
39719
39912
|
}
|
|
@@ -39757,6 +39950,9 @@ function parseRepositoryFilterExpression(expression) {
|
|
|
39757
39950
|
if (field === "status") {
|
|
39758
39951
|
normalizeStatus(value, "--filter");
|
|
39759
39952
|
}
|
|
39953
|
+
if (field === "scanning") {
|
|
39954
|
+
normalizeScanningInclusion(value, "--filter");
|
|
39955
|
+
}
|
|
39760
39956
|
return { field, operator, value };
|
|
39761
39957
|
}
|
|
39762
39958
|
function doesValueMatchFilter(candidate, filterValue, operator, exactMatchOnly) {
|
|
@@ -39764,7 +39960,7 @@ function doesValueMatchFilter(candidate, filterValue, operator, exactMatchOnly)
|
|
|
39764
39960
|
const normalizedFilter = normalizeForMatching(filterValue);
|
|
39765
39961
|
if (exactMatchOnly || operator === "exact" || operator === "not_exact") {
|
|
39766
39962
|
const matches2 = normalizedCandidate === normalizedFilter;
|
|
39767
|
-
return operator === "not_exact" ? !matches2 : matches2;
|
|
39963
|
+
return operator === "not_exact" || operator === "not_contains" ? !matches2 : matches2;
|
|
39768
39964
|
}
|
|
39769
39965
|
const matches = normalizedCandidate.includes(normalizedFilter);
|
|
39770
39966
|
return operator === "not_contains" ? !matches : matches;
|
|
@@ -39800,6 +39996,13 @@ function matchParsedFilter(repo, parsedFilter) {
|
|
|
39800
39996
|
const language = repo.primaryLanguage ?? "";
|
|
39801
39997
|
return doesValueMatchFilter(language, parsedFilter.value, parsedFilter.operator, false);
|
|
39802
39998
|
}
|
|
39999
|
+
case "scanning":
|
|
40000
|
+
return doesValueMatchFilter(
|
|
40001
|
+
scanningInclusionOf(repo),
|
|
40002
|
+
parsedFilter.value,
|
|
40003
|
+
parsedFilter.operator,
|
|
40004
|
+
true
|
|
40005
|
+
);
|
|
39803
40006
|
}
|
|
39804
40007
|
}
|
|
39805
40008
|
function filterRepositories(repositories, options) {
|
|
@@ -39811,6 +40014,7 @@ function filterRepositories(repositories, options) {
|
|
|
39811
40014
|
const normalizedQuery = options.query ? normalizeForMatching(options.query) : void 0;
|
|
39812
40015
|
const normalizedDefaultBranch = options.githubDefaultBranch ? normalizeForMatching(options.githubDefaultBranch) : void 0;
|
|
39813
40016
|
const normalizedLanguage = options.language ? normalizeForMatching(options.language) : void 0;
|
|
40017
|
+
const normalizedScanning = options.scanning ? normalizeScanningInclusion(options.scanning, "--scanning") : void 0;
|
|
39814
40018
|
return repositories.filter((repo) => {
|
|
39815
40019
|
if (normalizedPlatform && repo.platform.toUpperCase() !== normalizedPlatform) {
|
|
39816
40020
|
return false;
|
|
@@ -39818,6 +40022,9 @@ function filterRepositories(repositories, options) {
|
|
|
39818
40022
|
if (normalizedStatus && repo.status.toUpperCase() !== normalizedStatus) {
|
|
39819
40023
|
return false;
|
|
39820
40024
|
}
|
|
40025
|
+
if (normalizedScanning && scanningInclusionOf(repo) !== normalizedScanning) {
|
|
40026
|
+
return false;
|
|
40027
|
+
}
|
|
39821
40028
|
if (normalizedDefaultBranch && !normalizeForMatching(repo.githubDefaultBranch).includes(normalizedDefaultBranch)) {
|
|
39822
40029
|
return false;
|
|
39823
40030
|
}
|
|
@@ -39890,7 +40097,9 @@ function paginateResults(items, options) {
|
|
|
39890
40097
|
};
|
|
39891
40098
|
}
|
|
39892
40099
|
function printRepositoriesTsv(repositories) {
|
|
39893
|
-
console.log(
|
|
40100
|
+
console.log(
|
|
40101
|
+
"repositoryUrl platform status githubDefaultBranch scanningBranch language scanning"
|
|
40102
|
+
);
|
|
39894
40103
|
for (const repo of repositories) {
|
|
39895
40104
|
console.log(
|
|
39896
40105
|
[
|
|
@@ -39899,18 +40108,20 @@ function printRepositoriesTsv(repositories) {
|
|
|
39899
40108
|
tsvSafe(repo.status),
|
|
39900
40109
|
tsvSafe(repo.githubDefaultBranch),
|
|
39901
40110
|
tsvSafe(repo.scanningBranch ?? ""),
|
|
39902
|
-
tsvSafe(repo.primaryLanguage)
|
|
40111
|
+
tsvSafe(repo.primaryLanguage),
|
|
40112
|
+
scanningInclusionOf(repo)
|
|
39903
40113
|
].join(" ")
|
|
39904
40114
|
);
|
|
39905
40115
|
}
|
|
39906
40116
|
}
|
|
39907
40117
|
function buildRepositoriesTable(repositories) {
|
|
39908
|
-
const headers = ["Repository URL", "Platform", "Status"];
|
|
40118
|
+
const headers = ["Repository URL", "Platform", "Status", "Scanning"];
|
|
39909
40119
|
const rows = repositories.map(
|
|
39910
40120
|
(repo) => [
|
|
39911
40121
|
stripAnsi(String(repo.repositoryUrl)),
|
|
39912
40122
|
stripAnsi(String(repo.platform)).toUpperCase(),
|
|
39913
|
-
repo.status === "CONNECTED" ? "connected" : "disconnected"
|
|
40123
|
+
repo.status === "CONNECTED" ? "connected" : "disconnected",
|
|
40124
|
+
scanningInclusionOf(repo)
|
|
39914
40125
|
]
|
|
39915
40126
|
);
|
|
39916
40127
|
const colWidths = headers.map(
|
|
@@ -40039,13 +40250,27 @@ async function maybeRefreshCloneTokenSessionForOrganization(organizationId, list
|
|
|
40039
40250
|
}
|
|
40040
40251
|
return refreshForOrganization(workosOrganizationId);
|
|
40041
40252
|
}
|
|
40253
|
+
function resolveScanningFlag(options) {
|
|
40254
|
+
if (options.included && options.excluded) {
|
|
40255
|
+
throw new Error("Pass only one of --included / --excluded.");
|
|
40256
|
+
}
|
|
40257
|
+
if (options.included) {
|
|
40258
|
+
return "included";
|
|
40259
|
+
}
|
|
40260
|
+
if (options.excluded) {
|
|
40261
|
+
return "excluded";
|
|
40262
|
+
}
|
|
40263
|
+
return void 0;
|
|
40264
|
+
}
|
|
40042
40265
|
function toRepositoryListFilterOptions(options) {
|
|
40266
|
+
const scanning = resolveScanningFlag(options);
|
|
40043
40267
|
return {
|
|
40044
40268
|
...options.platform ? { platform: options.platform } : {},
|
|
40045
40269
|
...options.status ? { status: options.status } : {},
|
|
40046
40270
|
...options.query ? { query: options.query } : {},
|
|
40047
40271
|
...options.defaultBranch ? { githubDefaultBranch: options.defaultBranch } : {},
|
|
40048
40272
|
...options.language ? { language: options.language } : {},
|
|
40273
|
+
...scanning ? { scanning } : {},
|
|
40049
40274
|
...options.filter.length > 0 ? { filters: options.filter } : {}
|
|
40050
40275
|
};
|
|
40051
40276
|
}
|
|
@@ -40058,9 +40283,9 @@ function addRepositoryFilterOptions(command) {
|
|
|
40058
40283
|
"--status <status>",
|
|
40059
40284
|
"Filter by status (CONNECTED, DISCONNECTED)",
|
|
40060
40285
|
(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(
|
|
40286
|
+
).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
40287
|
"-f, --filter <expression>",
|
|
40063
|
-
"Advanced filter expression. Repeatable. Examples: platform=GITHUB, repo:trace, status!=DISCONNECTED",
|
|
40288
|
+
"Advanced filter expression. Repeatable. Examples: platform=GITHUB, repo:trace, status!=DISCONNECTED, scanning=excluded",
|
|
40064
40289
|
collectFilterExpression,
|
|
40065
40290
|
[]
|
|
40066
40291
|
).option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").option(
|
|
@@ -40088,6 +40313,14 @@ async function runRepositoryListCommand(options) {
|
|
|
40088
40313
|
}
|
|
40089
40314
|
return;
|
|
40090
40315
|
}
|
|
40316
|
+
let filterOptions;
|
|
40317
|
+
try {
|
|
40318
|
+
filterOptions = toRepositoryListFilterOptions(options);
|
|
40319
|
+
} catch (error48) {
|
|
40320
|
+
const message = error48 instanceof Error ? error48.message : "Unknown filter parsing error.";
|
|
40321
|
+
console.error(`Invalid repository filter: ${message}`);
|
|
40322
|
+
process.exit(1);
|
|
40323
|
+
}
|
|
40091
40324
|
const organizationId = await resolveOrganizationId();
|
|
40092
40325
|
if (options.dependency) {
|
|
40093
40326
|
await runDependencyFilter(organizationId, {
|
|
@@ -40111,14 +40344,14 @@ async function runRepositoryListCommand(options) {
|
|
|
40111
40344
|
process.exit(1);
|
|
40112
40345
|
}
|
|
40113
40346
|
try {
|
|
40114
|
-
filteredRepositories = filterRepositories(result.body,
|
|
40347
|
+
filteredRepositories = filterRepositories(result.body, filterOptions);
|
|
40115
40348
|
} catch (error48) {
|
|
40116
40349
|
const message = error48 instanceof Error ? error48.message : "Unknown filter parsing error.";
|
|
40117
40350
|
console.error(`Invalid repository filter: ${message}`);
|
|
40118
40351
|
process.exit(1);
|
|
40119
40352
|
}
|
|
40120
40353
|
if (filteredRepositories.length === 0) {
|
|
40121
|
-
if (options.platform || options.status || options.query || options.defaultBranch || options.language || options.filter.length > 0) {
|
|
40354
|
+
if (options.platform || options.status || options.query || options.defaultBranch || options.language || options.included || options.excluded || options.filter.length > 0) {
|
|
40122
40355
|
console.log("No repositories found matching the provided filters.");
|
|
40123
40356
|
return;
|
|
40124
40357
|
}
|
|
@@ -40137,7 +40370,7 @@ async function runRepositoryListCommand(options) {
|
|
|
40137
40370
|
process.exit(1);
|
|
40138
40371
|
}
|
|
40139
40372
|
if (options.output === "json") {
|
|
40140
|
-
|
|
40373
|
+
printJson(displayedRepositories);
|
|
40141
40374
|
return;
|
|
40142
40375
|
}
|
|
40143
40376
|
if (options.output === "tsv") {
|
|
@@ -40221,7 +40454,7 @@ async function runDependencyFilter(organizationId, options) {
|
|
|
40221
40454
|
process.exit(1);
|
|
40222
40455
|
}
|
|
40223
40456
|
if (options.output === "json") {
|
|
40224
|
-
|
|
40457
|
+
printJson(displayedRepos);
|
|
40225
40458
|
return;
|
|
40226
40459
|
}
|
|
40227
40460
|
if (options.output === "tsv") {
|
|
@@ -40252,6 +40485,8 @@ Examples:
|
|
|
40252
40485
|
tracecli repo list --output json | jq '.[].repositoryUrl'
|
|
40253
40486
|
tracecli repo list --output tsv | cut -f1
|
|
40254
40487
|
tracecli repo list --filter "language:typescript" --limit 10
|
|
40488
|
+
tracecli repo list --excluded
|
|
40489
|
+
tracecli repo list --query payments --included --output json
|
|
40255
40490
|
tracecli repo list --dependency lodash@4.17.21
|
|
40256
40491
|
tracecli repo list --web`
|
|
40257
40492
|
).action(async (options) => {
|
|
@@ -40269,6 +40504,7 @@ Examples:
|
|
|
40269
40504
|
tracecli repo filter --page 2 --limit 30
|
|
40270
40505
|
tracecli repo filter --all
|
|
40271
40506
|
tracecli repo filter --language typescript --limit 10
|
|
40507
|
+
tracecli repo filter --filter "scanning=excluded"
|
|
40272
40508
|
tracecli repo filter --output json | jq '.[].repositoryUrl'
|
|
40273
40509
|
tracecli repo filter --output tsv | cut -f1
|
|
40274
40510
|
tracecli repo filter --web`
|
|
@@ -40550,7 +40786,7 @@ async function runSetScanningBranchCommand(repoUrl, options) {
|
|
|
40550
40786
|
}
|
|
40551
40787
|
const urlById = new Map(listResult2.body.map((r2) => [r2.repositoryId, r2.repositoryUrl]));
|
|
40552
40788
|
if (options.output === "json") {
|
|
40553
|
-
|
|
40789
|
+
printJson(allResults);
|
|
40554
40790
|
return;
|
|
40555
40791
|
}
|
|
40556
40792
|
console.log("repositoryUrl status relabeledCount error");
|
|
@@ -40631,12 +40867,152 @@ Examples:
|
|
|
40631
40867
|
await runSetScanningBranchCommand(repositoryUrl, options);
|
|
40632
40868
|
});
|
|
40633
40869
|
}
|
|
40870
|
+
function resolveRepositoryReferences(repositories, references) {
|
|
40871
|
+
const byId = /* @__PURE__ */ new Map();
|
|
40872
|
+
const byUrl = /* @__PURE__ */ new Map();
|
|
40873
|
+
for (const repo of repositories) {
|
|
40874
|
+
byId.set(repo.repositoryId.toLowerCase(), repo);
|
|
40875
|
+
byUrl.set(normalizeRepoUrlForMatch(repo.repositoryUrl), repo);
|
|
40876
|
+
}
|
|
40877
|
+
const matched = /* @__PURE__ */ new Map();
|
|
40878
|
+
const unresolved = [];
|
|
40879
|
+
for (const reference of references) {
|
|
40880
|
+
const trimmed = reference.trim();
|
|
40881
|
+
if (trimmed.length === 0) {
|
|
40882
|
+
continue;
|
|
40883
|
+
}
|
|
40884
|
+
const repo = byId.get(trimmed.toLowerCase()) ?? byUrl.get(normalizeRepoUrlForMatch(resolveRepoUrl(trimmed)));
|
|
40885
|
+
if (!repo) {
|
|
40886
|
+
unresolved.push(trimmed);
|
|
40887
|
+
continue;
|
|
40888
|
+
}
|
|
40889
|
+
if (!matched.has(repo.repositoryId)) {
|
|
40890
|
+
matched.set(repo.repositoryId, repo);
|
|
40891
|
+
}
|
|
40892
|
+
}
|
|
40893
|
+
return { matched: [...matched.values()], unresolved };
|
|
40894
|
+
}
|
|
40895
|
+
function scanningInclusionVerb(includedInScanning) {
|
|
40896
|
+
return includedInScanning ? { past: "Included", noun: "included in scanning" } : { past: "Excluded", noun: "excluded from scanning" };
|
|
40897
|
+
}
|
|
40898
|
+
async function runSetScanningInclusionCommand(references, includedInScanning, options) {
|
|
40899
|
+
const organizationId = await resolveOrganizationId();
|
|
40900
|
+
const listResult = await authenticatedRequest(
|
|
40901
|
+
(client) => client.repository.listRepositories({ params: { organizationId } })
|
|
40902
|
+
);
|
|
40903
|
+
if (listResult.status === 401) {
|
|
40904
|
+
console.error("Authentication failed. Run `tracecli auth login` to re-authenticate.");
|
|
40905
|
+
process.exit(1);
|
|
40906
|
+
}
|
|
40907
|
+
if (listResult.status !== 200) {
|
|
40908
|
+
console.error(`Failed to list repositories (status ${listResult.status}).`);
|
|
40909
|
+
process.exit(1);
|
|
40910
|
+
}
|
|
40911
|
+
const { matched, unresolved } = resolveRepositoryReferences(listResult.body, references);
|
|
40912
|
+
if (unresolved.length > 0) {
|
|
40913
|
+
console.error(
|
|
40914
|
+
colorError(
|
|
40915
|
+
`${unresolved.length === 1 ? "Repository" : "Repositories"} not found in this organization: ${unresolved.join(", ")}`
|
|
40916
|
+
)
|
|
40917
|
+
);
|
|
40918
|
+
console.error(
|
|
40919
|
+
"Pass a repository ID, URL, or owner/repo shorthand. Run `tracecli repo list` to see connected repositories."
|
|
40920
|
+
);
|
|
40921
|
+
process.exit(1);
|
|
40922
|
+
}
|
|
40923
|
+
if (matched.length === 0) {
|
|
40924
|
+
console.error(colorError("No repositories given."));
|
|
40925
|
+
process.exit(1);
|
|
40926
|
+
}
|
|
40927
|
+
const outcomes = matched.map((repo) => ({
|
|
40928
|
+
repositoryId: repo.repositoryId,
|
|
40929
|
+
repositoryUrl: repo.repositoryUrl,
|
|
40930
|
+
previouslyIncludedInScanning: repo.includedInScanning
|
|
40931
|
+
}));
|
|
40932
|
+
const ids = matched.map((repo) => repo.repositoryId);
|
|
40933
|
+
let updatedCount = 0;
|
|
40934
|
+
const notFoundRepositoryIds = [];
|
|
40935
|
+
const verb = scanningInclusionVerb(includedInScanning);
|
|
40936
|
+
for (let offset = 0; offset < ids.length; offset += SCANNING_INCLUSION_BATCH_SIZE) {
|
|
40937
|
+
const result = await authenticatedRequest(
|
|
40938
|
+
(client) => client.repository.setScanningInclusion({
|
|
40939
|
+
params: { organizationId },
|
|
40940
|
+
body: {
|
|
40941
|
+
repositoryIds: ids.slice(offset, offset + SCANNING_INCLUSION_BATCH_SIZE),
|
|
40942
|
+
includedInScanning
|
|
40943
|
+
}
|
|
40944
|
+
})
|
|
40945
|
+
);
|
|
40946
|
+
if (result.status === 200) {
|
|
40947
|
+
updatedCount += result.body.updatedCount;
|
|
40948
|
+
notFoundRepositoryIds.push(...result.body.notFoundRepositoryIds);
|
|
40949
|
+
continue;
|
|
40950
|
+
}
|
|
40951
|
+
const committedSuffix = offset > 0 ? ` ${updatedCount} of ${ids.length} repositories were already updated.` : "";
|
|
40952
|
+
if (result.status === 401) {
|
|
40953
|
+
console.error("Authentication failed. Run `tracecli auth login` to re-authenticate.");
|
|
40954
|
+
} else if (result.status === 403) {
|
|
40955
|
+
console.error(
|
|
40956
|
+
colorError(
|
|
40957
|
+
`You don't have permission to change repository scanning in this organization (analyst or admin role required).${committedSuffix}`
|
|
40958
|
+
)
|
|
40959
|
+
);
|
|
40960
|
+
} else {
|
|
40961
|
+
const message = extractErrorMessage(result.body);
|
|
40962
|
+
console.error(
|
|
40963
|
+
colorError(
|
|
40964
|
+
`Failed to update repository scanning (status ${result.status})${message ? `: ${message}` : "."}${committedSuffix}`
|
|
40965
|
+
)
|
|
40966
|
+
);
|
|
40967
|
+
}
|
|
40968
|
+
process.exit(1);
|
|
40969
|
+
}
|
|
40970
|
+
if (options.output === "json") {
|
|
40971
|
+
printJson({ includedInScanning, updatedCount, notFoundRepositoryIds, repositories: outcomes });
|
|
40972
|
+
return;
|
|
40973
|
+
}
|
|
40974
|
+
const notFound = new Set(notFoundRepositoryIds);
|
|
40975
|
+
for (const outcome of outcomes) {
|
|
40976
|
+
if (notFound.has(outcome.repositoryId)) {
|
|
40977
|
+
console.log(
|
|
40978
|
+
`${colorWarningPrefix("Skipped")} ${outcome.repositoryUrl}: no longer connected to this organization`
|
|
40979
|
+
);
|
|
40980
|
+
} else if (outcome.previouslyIncludedInScanning === includedInScanning) {
|
|
40981
|
+
console.log(`Already ${verb.noun}: ${outcome.repositoryUrl}`);
|
|
40982
|
+
} else {
|
|
40983
|
+
console.log(`${colorSuccess(verb.past)} ${outcome.repositoryUrl}`);
|
|
40984
|
+
}
|
|
40985
|
+
}
|
|
40986
|
+
const changedCount = outcomes.filter(
|
|
40987
|
+
(outcome) => !notFound.has(outcome.repositoryId) && outcome.previouslyIncludedInScanning !== includedInScanning
|
|
40988
|
+
).length;
|
|
40989
|
+
console.log(
|
|
40990
|
+
`${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."}`
|
|
40991
|
+
);
|
|
40992
|
+
}
|
|
40993
|
+
function createScanningInclusionCommand(includedInScanning) {
|
|
40994
|
+
const name = includedInScanning ? "include" : "exclude";
|
|
40995
|
+
const description = includedInScanning ? "Include repositories in scanning" : "Exclude repositories from scanning (no scans of any kind run until included again)";
|
|
40996
|
+
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(
|
|
40997
|
+
"after",
|
|
40998
|
+
`
|
|
40999
|
+
Examples:
|
|
41000
|
+
tracecli repo ${name} acme/api
|
|
41001
|
+
tracecli repo ${name} https://github.com/acme/api https://bitbucket.org/acme/worker
|
|
41002
|
+
tracecli repo ${name} 3f1c6c2e-9b7a-4a6f-8f1d-2d4e5a6b7c8d --output json
|
|
41003
|
+
tracecli repo list --query legacy --output json | jq -r '.[].repositoryUrl' | xargs tracecli repo ${name}`
|
|
41004
|
+
).action(async (repositories, options) => {
|
|
41005
|
+
await runSetScanningInclusionCommand(repositories, includedInScanning, options);
|
|
41006
|
+
});
|
|
41007
|
+
}
|
|
40634
41008
|
function createRepoCommand() {
|
|
40635
41009
|
const repo = new Command("repo").description("Manage repositories");
|
|
40636
41010
|
repo.addCommand(createListCommand());
|
|
40637
41011
|
repo.addCommand(createFilterCommand());
|
|
40638
41012
|
repo.addCommand(createCloneCommand());
|
|
40639
41013
|
repo.addCommand(createSetScanningBranchCommand());
|
|
41014
|
+
repo.addCommand(createScanningInclusionCommand(true));
|
|
41015
|
+
repo.addCommand(createScanningInclusionCommand(false));
|
|
40640
41016
|
return repo;
|
|
40641
41017
|
}
|
|
40642
41018
|
|
|
@@ -40745,21 +41121,6 @@ function validateCreateTypeUrl(type, url2) {
|
|
|
40745
41121
|
}
|
|
40746
41122
|
return null;
|
|
40747
41123
|
}
|
|
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
41124
|
function buildCreateApplicationBody(input) {
|
|
40764
41125
|
return {
|
|
40765
41126
|
name: input.name,
|
|
@@ -40963,7 +41324,7 @@ function printApplicationRepositoriesTsv(links) {
|
|
|
40963
41324
|
}
|
|
40964
41325
|
function outputApplication(app, output) {
|
|
40965
41326
|
if (output === "json") {
|
|
40966
|
-
|
|
41327
|
+
printJson(app);
|
|
40967
41328
|
return;
|
|
40968
41329
|
}
|
|
40969
41330
|
if (output === "tsv") {
|
|
@@ -41048,7 +41409,7 @@ async function runList(options) {
|
|
|
41048
41409
|
}
|
|
41049
41410
|
const apps = result.body;
|
|
41050
41411
|
if (output === "json") {
|
|
41051
|
-
|
|
41412
|
+
printJson(apps);
|
|
41052
41413
|
return;
|
|
41053
41414
|
}
|
|
41054
41415
|
if (output === "tsv") {
|
|
@@ -41097,7 +41458,7 @@ async function runRepos(applicationId, options) {
|
|
|
41097
41458
|
}
|
|
41098
41459
|
const links = sortApplicationRepositoryLinks(result.body);
|
|
41099
41460
|
if (output === "json") {
|
|
41100
|
-
|
|
41461
|
+
printJson(links);
|
|
41101
41462
|
return;
|
|
41102
41463
|
}
|
|
41103
41464
|
if (output === "tsv") {
|
|
@@ -42802,7 +43163,7 @@ async function uploadLogo(organizationId, variant, file2) {
|
|
|
42802
43163
|
}
|
|
42803
43164
|
function outputOrganization(org, output, derived) {
|
|
42804
43165
|
if (output === "json") {
|
|
42805
|
-
|
|
43166
|
+
printJson(org);
|
|
42806
43167
|
return;
|
|
42807
43168
|
}
|
|
42808
43169
|
if (output === "tsv") {
|
|
@@ -43334,7 +43695,7 @@ Examples:
|
|
|
43334
43695
|
}
|
|
43335
43696
|
}
|
|
43336
43697
|
if (options.output === "json") {
|
|
43337
|
-
|
|
43698
|
+
printJson(info);
|
|
43338
43699
|
if (!info.loggedIn) {
|
|
43339
43700
|
process.exitCode = 1;
|
|
43340
43701
|
}
|
|
@@ -43473,7 +43834,7 @@ Examples:
|
|
|
43473
43834
|
);
|
|
43474
43835
|
const items = options.limit !== void 0 ? filteredItems.slice(0, options.limit) : filteredItems;
|
|
43475
43836
|
if (options.output === "json") {
|
|
43476
|
-
|
|
43837
|
+
printJson(items);
|
|
43477
43838
|
return;
|
|
43478
43839
|
}
|
|
43479
43840
|
if (options.output === "tsv") {
|
|
@@ -43549,16 +43910,10 @@ Examples:
|
|
|
43549
43910
|
const active = organizationContext.activeOrganization;
|
|
43550
43911
|
const source = organizationContext.selectedOrganizationId === active.organizationId ? "selected" : "default";
|
|
43551
43912
|
if (options.output === "json") {
|
|
43552
|
-
|
|
43553
|
-
|
|
43554
|
-
|
|
43555
|
-
|
|
43556
|
-
source
|
|
43557
|
-
},
|
|
43558
|
-
null,
|
|
43559
|
-
2
|
|
43560
|
-
)
|
|
43561
|
-
);
|
|
43913
|
+
printJson({
|
|
43914
|
+
...active,
|
|
43915
|
+
source
|
|
43916
|
+
});
|
|
43562
43917
|
return;
|
|
43563
43918
|
}
|
|
43564
43919
|
if (options.output === "tsv") {
|
|
@@ -43713,6 +44068,9 @@ function repoDisplayName(repositoryUrl) {
|
|
|
43713
44068
|
return repositoryUrl;
|
|
43714
44069
|
}
|
|
43715
44070
|
}
|
|
44071
|
+
function repoLabel(repo) {
|
|
44072
|
+
return sanitizeDisplay(repo.displayName ?? repoDisplayName(repo.repositoryUrl));
|
|
44073
|
+
}
|
|
43716
44074
|
function countedRows(totals) {
|
|
43717
44075
|
const b = totals.buckets;
|
|
43718
44076
|
return [
|
|
@@ -43769,7 +44127,7 @@ function renderCodeSizeTable(codeSize, options) {
|
|
|
43769
44127
|
lines.push(
|
|
43770
44128
|
...padValues(
|
|
43771
44129
|
shown.map((repo) => ({
|
|
43772
|
-
label:
|
|
44130
|
+
label: repoLabel(repo),
|
|
43773
44131
|
value: repo.billableSloc ?? 0
|
|
43774
44132
|
}))
|
|
43775
44133
|
)
|
|
@@ -43784,7 +44142,8 @@ function renderCodeSizeTsv(codeSize) {
|
|
|
43784
44142
|
return [
|
|
43785
44143
|
"repository billableSloc",
|
|
43786
44144
|
...codeSize.repositories.map(
|
|
43787
|
-
|
|
44145
|
+
// Upload names are customer-authored, and TSV lands in spreadsheets.
|
|
44146
|
+
(repo) => `${formulaSafe(repoLabel(repo))} ${repo.billableSloc === null ? "" : repo.billableSloc}`
|
|
43788
44147
|
)
|
|
43789
44148
|
];
|
|
43790
44149
|
}
|
|
@@ -43874,7 +44233,7 @@ Examples:
|
|
|
43874
44233
|
}
|
|
43875
44234
|
const codeSize = result.body;
|
|
43876
44235
|
if (options.output === "json") {
|
|
43877
|
-
|
|
44236
|
+
printJson(codeSize);
|
|
43878
44237
|
return;
|
|
43879
44238
|
}
|
|
43880
44239
|
if (options.output === "tsv") {
|
|
@@ -44527,7 +44886,7 @@ function parseEmailSince(value) {
|
|
|
44527
44886
|
}
|
|
44528
44887
|
function outputInbox(inbox, output) {
|
|
44529
44888
|
if (output === "json") {
|
|
44530
|
-
|
|
44889
|
+
printJson(inbox);
|
|
44531
44890
|
return;
|
|
44532
44891
|
}
|
|
44533
44892
|
if (output === "tsv") {
|
|
@@ -44572,7 +44931,7 @@ async function runList2(options) {
|
|
|
44572
44931
|
}
|
|
44573
44932
|
const { inboxes, quota } = result.body;
|
|
44574
44933
|
if (output === "json") {
|
|
44575
|
-
|
|
44934
|
+
printJson({ inboxes, quota });
|
|
44576
44935
|
return;
|
|
44577
44936
|
}
|
|
44578
44937
|
if (output === "tsv") {
|
|
@@ -44658,7 +45017,7 @@ async function runEmails(label, options) {
|
|
|
44658
45017
|
}
|
|
44659
45018
|
const { address, messages } = result.body;
|
|
44660
45019
|
if (output === "json") {
|
|
44661
|
-
|
|
45020
|
+
printJson(result.body);
|
|
44662
45021
|
return;
|
|
44663
45022
|
}
|
|
44664
45023
|
if (output === "tsv") {
|
|
@@ -44694,7 +45053,7 @@ async function runEmail(label, messageId, options) {
|
|
|
44694
45053
|
failReadOp2("read email", result.status, result.body, label);
|
|
44695
45054
|
}
|
|
44696
45055
|
if (output === "json") {
|
|
44697
|
-
|
|
45056
|
+
printJson(result.body);
|
|
44698
45057
|
return;
|
|
44699
45058
|
}
|
|
44700
45059
|
if (output === "tsv") {
|
|
@@ -45204,7 +45563,7 @@ function buildCredentialsTable(creds) {
|
|
|
45204
45563
|
}
|
|
45205
45564
|
function outputCredential(cred, output) {
|
|
45206
45565
|
if (output === "json") {
|
|
45207
|
-
|
|
45566
|
+
printJson(cred);
|
|
45208
45567
|
return;
|
|
45209
45568
|
}
|
|
45210
45569
|
if (output === "tsv") {
|
|
@@ -45404,7 +45763,7 @@ async function runList3(options) {
|
|
|
45404
45763
|
}
|
|
45405
45764
|
const creds = result.body;
|
|
45406
45765
|
if (output === "json") {
|
|
45407
|
-
|
|
45766
|
+
printJson(creds);
|
|
45408
45767
|
return;
|
|
45409
45768
|
}
|
|
45410
45769
|
if (output === "tsv") {
|
|
@@ -45605,7 +45964,7 @@ async function runMfaCode(credentialId, options) {
|
|
|
45605
45964
|
failMfaOp("generate a code for", result.status, result.body, credentialId);
|
|
45606
45965
|
}
|
|
45607
45966
|
if (output === "json") {
|
|
45608
|
-
|
|
45967
|
+
printJson(result.body);
|
|
45609
45968
|
return;
|
|
45610
45969
|
}
|
|
45611
45970
|
if (output === "tsv") {
|
|
@@ -45988,7 +46347,7 @@ function buildEnvironmentsTable(envs) {
|
|
|
45988
46347
|
}
|
|
45989
46348
|
function outputEnvironment(env2, output) {
|
|
45990
46349
|
if (output === "json") {
|
|
45991
|
-
|
|
46350
|
+
printJson(env2);
|
|
45992
46351
|
return;
|
|
45993
46352
|
}
|
|
45994
46353
|
if (output === "tsv") {
|
|
@@ -46110,7 +46469,7 @@ async function runListAcrossOrganization(output, pager) {
|
|
|
46110
46469
|
createOrgEnvironmentRequests(organizationId)
|
|
46111
46470
|
);
|
|
46112
46471
|
if (output === "json") {
|
|
46113
|
-
|
|
46472
|
+
printJson(envs);
|
|
46114
46473
|
return;
|
|
46115
46474
|
}
|
|
46116
46475
|
if (output === "tsv") {
|
|
@@ -46150,7 +46509,7 @@ async function runList4(options) {
|
|
|
46150
46509
|
}
|
|
46151
46510
|
const envs = result.body;
|
|
46152
46511
|
if (output === "json") {
|
|
46153
|
-
|
|
46512
|
+
printJson(envs);
|
|
46154
46513
|
return;
|
|
46155
46514
|
}
|
|
46156
46515
|
if (output === "tsv") {
|
|
@@ -46644,7 +47003,7 @@ async function runCreateFinding(options) {
|
|
|
46644
47003
|
failCreateFinding(result.status, result.body, options.pentest);
|
|
46645
47004
|
}
|
|
46646
47005
|
if (options.output === "json") {
|
|
46647
|
-
|
|
47006
|
+
printJson(result.body);
|
|
46648
47007
|
return;
|
|
46649
47008
|
}
|
|
46650
47009
|
printCreatedFinding({
|
|
@@ -46657,185 +47016,159 @@ async function runCreateFinding(options) {
|
|
|
46657
47016
|
});
|
|
46658
47017
|
}
|
|
46659
47018
|
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(
|
|
47019
|
+
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(
|
|
47020
|
+
"--cvss-vector <vector>",
|
|
47021
|
+
"CVSS base vector (CVSS:3.1/... or CVSS:4.0/...); the score is derived server-side"
|
|
47022
|
+
).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
47023
|
await runCreateFinding(options);
|
|
46662
47024
|
});
|
|
46663
47025
|
}
|
|
46664
47026
|
|
|
46665
|
-
// src/pentest/
|
|
46666
|
-
import {
|
|
47027
|
+
// src/pentest/deliverables.ts
|
|
47028
|
+
import { open as open2, rename, rm, stat } from "fs/promises";
|
|
47029
|
+
import { basename as basename3, join as join6, resolve } from "path";
|
|
47030
|
+
|
|
47031
|
+
// src/upload/prepare.ts
|
|
47032
|
+
import { createHash } from "crypto";
|
|
47033
|
+
import { createReadStream, statSync as statSync3 } from "fs";
|
|
47034
|
+
import { open } from "fs/promises";
|
|
46667
47035
|
import { basename as basename2 } from "path";
|
|
46668
|
-
var
|
|
46669
|
-
|
|
46670
|
-
|
|
46671
|
-
|
|
47036
|
+
var CodeUploadPrepareError = class extends Error {
|
|
47037
|
+
constructor(message) {
|
|
47038
|
+
super(message);
|
|
47039
|
+
this.name = "CodeUploadPrepareError";
|
|
47040
|
+
}
|
|
47041
|
+
};
|
|
47042
|
+
var KIND_BY_INPUT = {
|
|
47043
|
+
source: "SOURCE_ARCHIVE",
|
|
47044
|
+
code: "SOURCE_ARCHIVE",
|
|
47045
|
+
"source-archive": "SOURCE_ARCHIVE",
|
|
47046
|
+
source_archive: "SOURCE_ARCHIVE",
|
|
47047
|
+
SOURCE_ARCHIVE: "SOURCE_ARCHIVE",
|
|
47048
|
+
apk: "ANDROID_APK",
|
|
47049
|
+
android: "ANDROID_APK",
|
|
47050
|
+
"android-apk": "ANDROID_APK",
|
|
47051
|
+
android_apk: "ANDROID_APK",
|
|
47052
|
+
ANDROID_APK: "ANDROID_APK",
|
|
47053
|
+
ipa: "IOS_IPA",
|
|
47054
|
+
ios: "IOS_IPA",
|
|
47055
|
+
"ios-ipa": "IOS_IPA",
|
|
47056
|
+
ios_ipa: "IOS_IPA",
|
|
47057
|
+
IOS_IPA: "IOS_IPA"
|
|
47058
|
+
};
|
|
47059
|
+
function normalizeArtifactKind(input) {
|
|
47060
|
+
const trimmed = input.trim();
|
|
47061
|
+
return KIND_BY_INPUT[trimmed] ?? KIND_BY_INPUT[trimmed.toLowerCase()] ?? null;
|
|
46672
47062
|
}
|
|
46673
|
-
function
|
|
46674
|
-
|
|
47063
|
+
function inferArtifactKindFromFilename(filename) {
|
|
47064
|
+
const lower = filename.toLowerCase();
|
|
47065
|
+
if (lower.endsWith(".zip")) return "SOURCE_ARCHIVE";
|
|
47066
|
+
if (lower.endsWith(".apk")) return "ANDROID_APK";
|
|
47067
|
+
if (lower.endsWith(".ipa")) return "IOS_IPA";
|
|
47068
|
+
return null;
|
|
46675
47069
|
}
|
|
46676
|
-
function
|
|
46677
|
-
if (
|
|
46678
|
-
if (
|
|
46679
|
-
return
|
|
47070
|
+
function requiredExtensionForKind(kind) {
|
|
47071
|
+
if (kind === "ANDROID_APK") return ".apk";
|
|
47072
|
+
if (kind === "IOS_IPA") return ".ipa";
|
|
47073
|
+
return ".zip";
|
|
46680
47074
|
}
|
|
46681
|
-
|
|
46682
|
-
|
|
46683
|
-
|
|
47075
|
+
function defaultDisplayName(originalFilename) {
|
|
47076
|
+
const base = originalFilename.replace(/\.(zip|apk|ipa)$/i, "").trim();
|
|
47077
|
+
return base.slice(0, 200) || "source";
|
|
47078
|
+
}
|
|
47079
|
+
function formatArchiveBytes(bytes) {
|
|
47080
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
47081
|
+
if (bytes < 1024 * 1024) {
|
|
47082
|
+
const kb = bytes / 1024;
|
|
47083
|
+
return `${kb.toFixed(Number.isInteger(kb) ? 0 : 1)} KB`;
|
|
46684
47084
|
}
|
|
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();
|
|
47085
|
+
if (bytes < 1024 * 1024 * 1024) {
|
|
47086
|
+
const mb = bytes / (1024 * 1024);
|
|
47087
|
+
return `${mb.toFixed(Number.isInteger(mb) ? 0 : 1)} MB`;
|
|
46700
47088
|
}
|
|
46701
|
-
|
|
46702
|
-
|
|
46703
|
-
|
|
47089
|
+
const gb = bytes / (1024 * 1024 * 1024);
|
|
47090
|
+
return `${gb.toFixed(Number.isInteger(gb) ? 0 : 1)} GB`;
|
|
47091
|
+
}
|
|
47092
|
+
function archiveTooLargeMessage() {
|
|
47093
|
+
return `The file must be ${formatArchiveBytes(CODE_UPLOAD_MAX_BYTES)} or smaller.`;
|
|
47094
|
+
}
|
|
47095
|
+
async function hashFileSha256(filePath) {
|
|
47096
|
+
const hash2 = createHash("sha256");
|
|
47097
|
+
const stream = createReadStream(filePath);
|
|
47098
|
+
for await (const chunk of stream) {
|
|
47099
|
+
hash2.update(chunk);
|
|
46704
47100
|
}
|
|
46705
|
-
|
|
46706
|
-
|
|
46707
|
-
|
|
46708
|
-
|
|
46709
|
-
|
|
46710
|
-
|
|
47101
|
+
return hash2.digest("hex");
|
|
47102
|
+
}
|
|
47103
|
+
async function readFileSlice(filePath, start, endExclusive) {
|
|
47104
|
+
const length = endExclusive - start;
|
|
47105
|
+
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(length) || start < 0 || length < 1) {
|
|
47106
|
+
throw new Error(
|
|
47107
|
+
`Invalid file slice: start=${String(start)} endExclusive=${String(endExclusive)}.`
|
|
46711
47108
|
);
|
|
46712
|
-
process.exit(1);
|
|
46713
47109
|
}
|
|
46714
|
-
|
|
46715
|
-
|
|
46716
|
-
|
|
46717
|
-
|
|
46718
|
-
|
|
46719
|
-
|
|
46720
|
-
|
|
47110
|
+
const buffer = Buffer.alloc(length);
|
|
47111
|
+
const handle = await open(filePath, "r");
|
|
47112
|
+
try {
|
|
47113
|
+
const { bytesRead } = await handle.read(buffer, 0, length, start);
|
|
47114
|
+
if (bytesRead !== length) {
|
|
47115
|
+
throw new Error(
|
|
47116
|
+
`Expected to read ${String(length)} bytes at offset ${String(start)}, got ${String(bytesRead)}.`
|
|
47117
|
+
);
|
|
47118
|
+
}
|
|
47119
|
+
return buffer;
|
|
47120
|
+
} finally {
|
|
47121
|
+
await handle.close();
|
|
46721
47122
|
}
|
|
46722
|
-
|
|
46723
|
-
|
|
46724
|
-
|
|
46725
|
-
|
|
47123
|
+
}
|
|
47124
|
+
function assertPreparedMatchesUpload(upload, prepared) {
|
|
47125
|
+
if (upload.sizeBytes !== prepared.sizeBytes || upload.archiveSha256 !== prepared.archiveSha256) {
|
|
47126
|
+
throw new CodeUploadPrepareError(
|
|
47127
|
+
"The file does not match the interrupted upload (size or SHA-256 differs)."
|
|
47128
|
+
);
|
|
46726
47129
|
}
|
|
47130
|
+
}
|
|
47131
|
+
async function prepareUploadFile(filePath, options = {}) {
|
|
46727
47132
|
let sizeBytes;
|
|
46728
47133
|
try {
|
|
46729
|
-
const stats = statSync3(
|
|
47134
|
+
const stats = statSync3(filePath);
|
|
46730
47135
|
if (!stats.isFile()) {
|
|
46731
|
-
|
|
46732
|
-
process.exit(1);
|
|
47136
|
+
throw new CodeUploadPrepareError("Path is not a regular file. Pass a .zip, .apk, or .ipa.");
|
|
46733
47137
|
}
|
|
46734
47138
|
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);
|
|
47139
|
+
} catch (error48) {
|
|
47140
|
+
if (error48 instanceof CodeUploadPrepareError) throw error48;
|
|
47141
|
+
throw new CodeUploadPrepareError("Cannot read that file.");
|
|
46771
47142
|
}
|
|
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);
|
|
47143
|
+
if (!Number.isSafeInteger(sizeBytes) || sizeBytes < 1) {
|
|
47144
|
+
throw new CodeUploadPrepareError("The file is empty.");
|
|
46781
47145
|
}
|
|
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();
|
|
47146
|
+
if (sizeBytes > CODE_UPLOAD_MAX_BYTES) {
|
|
47147
|
+
throw new CodeUploadPrepareError(archiveTooLargeMessage());
|
|
46796
47148
|
}
|
|
46797
|
-
|
|
46798
|
-
|
|
46799
|
-
|
|
46800
|
-
|
|
46801
|
-
);
|
|
46802
|
-
process.exit(1);
|
|
47149
|
+
const originalFilename = basename2(filePath);
|
|
47150
|
+
const inferred = inferArtifactKindFromFilename(originalFilename);
|
|
47151
|
+
const artifactKind = options.artifactKind ?? inferred;
|
|
47152
|
+
if (artifactKind == null) {
|
|
47153
|
+
throw new CodeUploadPrepareError("Pass a .zip source archive, an .apk, or an .ipa.");
|
|
46803
47154
|
}
|
|
46804
|
-
|
|
46805
|
-
|
|
46806
|
-
|
|
47155
|
+
const expected = requiredExtensionForKind(artifactKind);
|
|
47156
|
+
if (!originalFilename.toLowerCase().endsWith(expected)) {
|
|
47157
|
+
const label = isApplicationBuildArtifactKind(artifactKind) ? artifactKind === "ANDROID_APK" ? "Android APK" : "iOS package" : "source archive";
|
|
47158
|
+
throw new CodeUploadPrepareError(`A ${label} must end in ${expected}.`);
|
|
46807
47159
|
}
|
|
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.");
|
|
47160
|
+
const displayName = (options.displayName ?? defaultDisplayName(originalFilename)).trim();
|
|
47161
|
+
if (displayName.length < 1 || displayName.length > 200) {
|
|
47162
|
+
throw new CodeUploadPrepareError("Display name must be 1 to 200 characters.");
|
|
46820
47163
|
}
|
|
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
|
-
});
|
|
47164
|
+
return {
|
|
47165
|
+
path: filePath,
|
|
47166
|
+
displayName,
|
|
47167
|
+
originalFilename,
|
|
47168
|
+
sizeBytes,
|
|
47169
|
+
archiveSha256: await hashFileSha256(filePath),
|
|
47170
|
+
artifactKind
|
|
47171
|
+
};
|
|
46839
47172
|
}
|
|
46840
47173
|
|
|
46841
47174
|
// src/pentest/status-update.ts
|
|
@@ -46958,64 +47291,734 @@ async function runStatusUpdate(options) {
|
|
|
46958
47291
|
);
|
|
46959
47292
|
process.exit(1);
|
|
46960
47293
|
}
|
|
46961
|
-
const viaApiKey = requireAuth().kind === "api_key";
|
|
46962
|
-
const organizationId = await resolveOrganizationId();
|
|
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({
|
|
47294
|
+
const viaApiKey = requireAuth().kind === "api_key";
|
|
47295
|
+
const organizationId = await resolveOrganizationId();
|
|
47296
|
+
const params = {
|
|
47297
|
+
organizationId,
|
|
47298
|
+
pentestId: options.pentest,
|
|
47299
|
+
pentestFindingId: options.finding
|
|
47300
|
+
};
|
|
47301
|
+
const result = viaApiKey ? await authenticatedRequest(
|
|
47302
|
+
(client) => client.pentestFinding.automationStatusUpdate({
|
|
47303
|
+
params,
|
|
47304
|
+
body: { status: target, ...note !== void 0 ? { note } : {} }
|
|
47305
|
+
})
|
|
47306
|
+
) : target === "IN_PROGRESS" ? await authenticatedRequest(
|
|
47307
|
+
(client) => client.pentestFinding.startProgress({ params, body: {} })
|
|
47308
|
+
) : await authenticatedRequest(
|
|
47309
|
+
(client) => client.pentestFinding.reportRemediation({
|
|
47310
|
+
params,
|
|
47311
|
+
body: note !== void 0 ? { note } : {}
|
|
47312
|
+
})
|
|
47313
|
+
);
|
|
47314
|
+
if (result.status !== 200) {
|
|
47315
|
+
failStatusUpdate(result.status, result.body, viaApiKey);
|
|
47316
|
+
}
|
|
47317
|
+
if (options.output === "json") {
|
|
47318
|
+
printJson(result.body);
|
|
47319
|
+
return;
|
|
47320
|
+
}
|
|
47321
|
+
const displayId = result.body.displayId;
|
|
47322
|
+
const status = sanitizeDisplay(result.body.status);
|
|
47323
|
+
console.log(colorSuccess(`PTF-${displayId} is now ${status}`));
|
|
47324
|
+
}
|
|
47325
|
+
function createStatusUpdateCommand() {
|
|
47326
|
+
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(
|
|
47327
|
+
"--status <status>",
|
|
47328
|
+
"IN_PROGRESS (start work) or REMEDIATED (ready for retest)"
|
|
47329
|
+
).option("--note <text>", "Optional context when reporting remediation").option("-o, --output <format>", "Output format: text, json (default: text)", "text").addHelpText(
|
|
47330
|
+
"after",
|
|
47331
|
+
`
|
|
47332
|
+
Customer-facing status writes \u2014 the same transitions as the dashboard. An agent
|
|
47333
|
+
can mark a finding in progress when it starts the fix, then remediated when the
|
|
47334
|
+
fix is ready for Trace to re-test. Leaving the finding IN_PROGRESS (and not
|
|
47335
|
+
sending REMEDIATED) keeps it out of the retest queue.
|
|
47336
|
+
|
|
47337
|
+
IN_PROGRESS is allowed from OPEN or VERIFIED_OPEN. REMEDIATED is allowed from
|
|
47338
|
+
OPEN, IN_PROGRESS, or VERIFIED_OPEN.
|
|
47339
|
+
|
|
47340
|
+
A signed-in session (\`tracecli auth login\`) attributes the change to you. A
|
|
47341
|
+
Trace API key works too, once Trace enables finding automation for your
|
|
47342
|
+
organization: those writes are recorded as \`api-key:<key name>\` and show up as
|
|
47343
|
+
yours in the finding's history.
|
|
47344
|
+
|
|
47345
|
+
Examples:
|
|
47346
|
+
tracecli pentest findings status-update --pentest PT-3 --finding 7 --status IN_PROGRESS
|
|
47347
|
+
tracecli pentest findings status-update --pentest PT-3 --finding PTF-7 --status REMEDIATED
|
|
47348
|
+
tracecli pentest findings status-update --pentest PT-3 --finding 7 --status REMEDIATED \\
|
|
47349
|
+
--note "Fixed in PR 412" --output json`
|
|
47350
|
+
).action(async (options) => {
|
|
47351
|
+
await runStatusUpdate(options);
|
|
47352
|
+
});
|
|
47353
|
+
}
|
|
47354
|
+
|
|
47355
|
+
// src/pentest/deliverables.ts
|
|
47356
|
+
var PentestDeliverableDownloadError = class extends Error {
|
|
47357
|
+
constructor(message) {
|
|
47358
|
+
super(message);
|
|
47359
|
+
this.name = "PentestDeliverableDownloadError";
|
|
47360
|
+
}
|
|
47361
|
+
};
|
|
47362
|
+
function safeDeliverableFilename(documentId, extension, fallbackStem) {
|
|
47363
|
+
const stripped = Array.from(basename3(documentId.replace(/\\/g, "/")), (char) => {
|
|
47364
|
+
const code = char.codePointAt(0) ?? 0;
|
|
47365
|
+
return code <= 31 || code === 127 ? "_" : char;
|
|
47366
|
+
}).join("").trim();
|
|
47367
|
+
const stem = stripped.length === 0 || stripped === "." || stripped === ".." ? fallbackStem : stripped;
|
|
47368
|
+
return stem.toLowerCase().endsWith(extension) ? stem : `${stem}${extension}`;
|
|
47369
|
+
}
|
|
47370
|
+
function reportFileName(report, artifact) {
|
|
47371
|
+
if (artifact === "docx") {
|
|
47372
|
+
return safeDeliverableFilename(report.documentId, ".docx", "pentest-report");
|
|
47373
|
+
}
|
|
47374
|
+
const extension = report.downloadFormat === "bundle" ? ".zip" : ".pdf";
|
|
47375
|
+
return safeDeliverableFilename(report.documentId, extension, "pentest-report");
|
|
47376
|
+
}
|
|
47377
|
+
function attestationFileName(attestation) {
|
|
47378
|
+
return safeDeliverableFilename(attestation.documentId, ".pdf", "letter-of-attestation");
|
|
47379
|
+
}
|
|
47380
|
+
async function resolveDeliverableDestination(fileName, outOption) {
|
|
47381
|
+
if (outOption === void 0) return resolve(process.cwd(), fileName);
|
|
47382
|
+
const target = resolve(outOption);
|
|
47383
|
+
try {
|
|
47384
|
+
const info = await stat(target);
|
|
47385
|
+
if (info.isDirectory()) return join6(target, fileName);
|
|
47386
|
+
} catch {
|
|
47387
|
+
}
|
|
47388
|
+
return target;
|
|
47389
|
+
}
|
|
47390
|
+
async function pathExists(path) {
|
|
47391
|
+
try {
|
|
47392
|
+
await stat(path);
|
|
47393
|
+
return true;
|
|
47394
|
+
} catch {
|
|
47395
|
+
return false;
|
|
47396
|
+
}
|
|
47397
|
+
}
|
|
47398
|
+
async function downloadDeliverableToFile(downloadUrl, destination, options = {}) {
|
|
47399
|
+
const fetchImpl = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
|
|
47400
|
+
const partialPath = `${destination}.part`;
|
|
47401
|
+
let response;
|
|
47402
|
+
try {
|
|
47403
|
+
response = await fetchImpl(downloadUrl, { method: "GET", redirect: "manual" });
|
|
47404
|
+
} catch (error48) {
|
|
47405
|
+
const detail = error48 instanceof Error ? error48.message : "network error";
|
|
47406
|
+
throw new PentestDeliverableDownloadError(`Download from storage failed: ${detail}.`);
|
|
47407
|
+
}
|
|
47408
|
+
if (!response.ok || response.body === null) {
|
|
47409
|
+
if (response.body !== null) await response.body.cancel();
|
|
47410
|
+
throw new PentestDeliverableDownloadError(
|
|
47411
|
+
`Storage answered HTTP ${response.status} instead of the document. Retry; the download link may have expired.`
|
|
47412
|
+
);
|
|
47413
|
+
}
|
|
47414
|
+
const contentLength = response.headers.get("content-length");
|
|
47415
|
+
let totalBytes = null;
|
|
47416
|
+
if (contentLength !== null) {
|
|
47417
|
+
const declared = Number(contentLength);
|
|
47418
|
+
if (!Number.isSafeInteger(declared) || declared < 0) {
|
|
47419
|
+
await response.body.cancel();
|
|
47420
|
+
throw new PentestDeliverableDownloadError(
|
|
47421
|
+
`Storage declared an unreadable Content-Length ('${contentLength}').`
|
|
47422
|
+
);
|
|
47423
|
+
}
|
|
47424
|
+
totalBytes = declared;
|
|
47425
|
+
}
|
|
47426
|
+
await rm(partialPath, { force: true });
|
|
47427
|
+
const file2 = await open2(partialPath, "wx");
|
|
47428
|
+
const reader = response.body.getReader();
|
|
47429
|
+
let downloadedBytes = 0;
|
|
47430
|
+
options.onProgress?.({ downloadedBytes, totalBytes });
|
|
47431
|
+
try {
|
|
47432
|
+
for (; ; ) {
|
|
47433
|
+
const chunk = await reader.read();
|
|
47434
|
+
if (chunk.done) break;
|
|
47435
|
+
downloadedBytes += chunk.value.byteLength;
|
|
47436
|
+
if (totalBytes !== null && downloadedBytes > totalBytes) {
|
|
47437
|
+
await reader.cancel();
|
|
47438
|
+
throw new PentestDeliverableDownloadError(
|
|
47439
|
+
`Storage sent more than the ${totalBytes} bytes it declared.`
|
|
47440
|
+
);
|
|
47441
|
+
}
|
|
47442
|
+
await writeAll(file2, chunk.value);
|
|
47443
|
+
options.onProgress?.({ downloadedBytes, totalBytes });
|
|
47444
|
+
}
|
|
47445
|
+
await file2.close();
|
|
47446
|
+
if (totalBytes !== null && downloadedBytes !== totalBytes) {
|
|
47447
|
+
throw new PentestDeliverableDownloadError(
|
|
47448
|
+
`Download ended after ${downloadedBytes} of ${totalBytes} bytes.`
|
|
47449
|
+
);
|
|
47450
|
+
}
|
|
47451
|
+
if (downloadedBytes === 0) {
|
|
47452
|
+
throw new PentestDeliverableDownloadError("Storage returned an empty document.");
|
|
47453
|
+
}
|
|
47454
|
+
await rename(partialPath, destination);
|
|
47455
|
+
return { bytes: downloadedBytes };
|
|
47456
|
+
} catch (error48) {
|
|
47457
|
+
await file2.close().catch(() => void 0);
|
|
47458
|
+
await rm(partialPath, { force: true });
|
|
47459
|
+
if (error48 instanceof PentestDeliverableDownloadError) throw error48;
|
|
47460
|
+
const detail = error48 instanceof Error ? error48.message : "unknown error";
|
|
47461
|
+
throw new PentestDeliverableDownloadError(`Download from storage failed: ${detail}.`);
|
|
47462
|
+
}
|
|
47463
|
+
}
|
|
47464
|
+
async function writeAll(file2, bytes) {
|
|
47465
|
+
let offset = 0;
|
|
47466
|
+
while (offset < bytes.byteLength) {
|
|
47467
|
+
const { bytesWritten } = await file2.write(bytes, offset, bytes.byteLength - offset);
|
|
47468
|
+
if (bytesWritten === 0) {
|
|
47469
|
+
throw new PentestDeliverableDownloadError("Writing the document to disk stalled.");
|
|
47470
|
+
}
|
|
47471
|
+
offset += bytesWritten;
|
|
47472
|
+
}
|
|
47473
|
+
}
|
|
47474
|
+
var DELIVERABLE_LABEL = {
|
|
47475
|
+
report: "report",
|
|
47476
|
+
attestation: "letter of attestation"
|
|
47477
|
+
};
|
|
47478
|
+
function requirePentestIdentifier(value) {
|
|
47479
|
+
if (!isPentestIdentifier(value)) {
|
|
47480
|
+
console.error(
|
|
47481
|
+
colorError(`Invalid pentest identifier '${value}'. Use a display ID (e.g. PT-3) or UUID.`)
|
|
47482
|
+
);
|
|
47483
|
+
process.exit(1);
|
|
47484
|
+
}
|
|
47485
|
+
}
|
|
47486
|
+
function failDeliverable(status, body, deliverable, pentestIdentifier, action) {
|
|
47487
|
+
const { message, errorCode } = readErrorFields(body);
|
|
47488
|
+
const label = DELIVERABLE_LABEL[deliverable];
|
|
47489
|
+
if (status === UNAUTHORIZED_STATUS2) {
|
|
47490
|
+
exitAuthFailed();
|
|
47491
|
+
}
|
|
47492
|
+
if (status === 403) {
|
|
47493
|
+
console.error(
|
|
47494
|
+
colorError(
|
|
47495
|
+
message ?? `You do not have permission to ${action === "list" ? "view" : "download"} ${label}s for this organization.`
|
|
47496
|
+
)
|
|
47497
|
+
);
|
|
47498
|
+
process.exit(1);
|
|
47499
|
+
}
|
|
47500
|
+
if (status === 404) {
|
|
47501
|
+
if (message === "Report not found") {
|
|
47502
|
+
console.error(
|
|
47503
|
+
colorError(
|
|
47504
|
+
`No report has been shared for pentest ${pentestIdentifier} yet. Trace publishes it to your dashboard when the engagement's report is ready.`
|
|
47505
|
+
)
|
|
47506
|
+
);
|
|
47507
|
+
process.exit(1);
|
|
47508
|
+
}
|
|
47509
|
+
if (message === "Attestation not found") {
|
|
47510
|
+
console.error(
|
|
47511
|
+
colorError(
|
|
47512
|
+
`That letter of attestation is not available for pentest ${pentestIdentifier}. Run \`tracecli pentest attestation list ${pentestIdentifier}\` to see the letters shared with you.`
|
|
47513
|
+
)
|
|
47514
|
+
);
|
|
47515
|
+
process.exit(1);
|
|
47516
|
+
}
|
|
47517
|
+
console.error(
|
|
47518
|
+
colorError(
|
|
47519
|
+
`Pentest '${pentestIdentifier}' not found. Verify the ID with \`tracecli pentest list\`.`
|
|
47520
|
+
)
|
|
47521
|
+
);
|
|
47522
|
+
process.exit(1);
|
|
47523
|
+
}
|
|
47524
|
+
if (status === 409) {
|
|
47525
|
+
if (errorCode === "DOCX_NOT_RENDERED") {
|
|
47526
|
+
console.error(
|
|
47527
|
+
colorError(
|
|
47528
|
+
"No Word document is available for this report. Drop --docx to download the PDF."
|
|
47529
|
+
)
|
|
47530
|
+
);
|
|
47531
|
+
process.exit(1);
|
|
47532
|
+
}
|
|
47533
|
+
console.error(colorError(message ?? `Conflict (status ${status}).`));
|
|
47534
|
+
process.exit(1);
|
|
47535
|
+
}
|
|
47536
|
+
if (status === 400) {
|
|
47537
|
+
console.error(colorError(message ?? "Invalid request (status 400)."));
|
|
47538
|
+
process.exit(1);
|
|
47539
|
+
}
|
|
47540
|
+
console.error(
|
|
47541
|
+
colorError(
|
|
47542
|
+
`Failed to ${action} ${label}${action === "list" ? "s" : ""} (status ${status})${message ? `: ${message}` : "."}`
|
|
47543
|
+
)
|
|
47544
|
+
);
|
|
47545
|
+
process.exit(1);
|
|
47546
|
+
}
|
|
47547
|
+
function writeDownloadProgress(progress3) {
|
|
47548
|
+
if (!process.stderr.isTTY) return;
|
|
47549
|
+
const downloaded = formatArchiveBytes(progress3.downloadedBytes);
|
|
47550
|
+
if (progress3.totalBytes === null || progress3.totalBytes === 0) {
|
|
47551
|
+
process.stderr.write(`\rDownloading ${downloaded}`);
|
|
47552
|
+
return;
|
|
47553
|
+
}
|
|
47554
|
+
const pct = Math.floor(progress3.downloadedBytes / progress3.totalBytes * 100);
|
|
47555
|
+
process.stderr.write(
|
|
47556
|
+
`\rDownloading ${pct}% (${downloaded} / ${formatArchiveBytes(progress3.totalBytes)})`
|
|
47557
|
+
);
|
|
47558
|
+
}
|
|
47559
|
+
function finishProgress() {
|
|
47560
|
+
if (process.stderr.isTTY) process.stderr.write("\n");
|
|
47561
|
+
}
|
|
47562
|
+
async function saveDeliverable(downloadUrl, fileName, options) {
|
|
47563
|
+
const destination = await resolveDeliverableDestination(fileName, options.out);
|
|
47564
|
+
if (!options.force && await pathExists(destination)) {
|
|
47565
|
+
console.error(
|
|
47566
|
+
colorError(`${destination} already exists. Pass --force to overwrite it or --out <path>.`)
|
|
47567
|
+
);
|
|
47568
|
+
process.exit(1);
|
|
47569
|
+
}
|
|
47570
|
+
if (options.output === "table") {
|
|
47571
|
+
console.error(`Downloading ${sanitizeDisplay(fileName)} to ${destination}`);
|
|
47572
|
+
}
|
|
47573
|
+
try {
|
|
47574
|
+
const { bytes } = await downloadDeliverableToFile(downloadUrl, destination, {
|
|
47575
|
+
onProgress: writeDownloadProgress
|
|
47576
|
+
});
|
|
47577
|
+
finishProgress();
|
|
47578
|
+
return { destination, bytes };
|
|
47579
|
+
} catch (error48) {
|
|
47580
|
+
finishProgress();
|
|
47581
|
+
const detail = error48 instanceof Error ? error48.message : "Download from storage failed.";
|
|
47582
|
+
console.error(colorError(detail));
|
|
47583
|
+
process.exit(1);
|
|
47584
|
+
}
|
|
47585
|
+
}
|
|
47586
|
+
async function runReportDownload(pentestIdentifier, options) {
|
|
47587
|
+
const output = parseOutput(options.output);
|
|
47588
|
+
requirePentestIdentifier(pentestIdentifier);
|
|
47589
|
+
const artifact = options.docx ? "docx" : "primary";
|
|
47590
|
+
const organizationId = await resolveOrganizationId();
|
|
47591
|
+
const result = await authenticatedRequest(
|
|
47592
|
+
(client) => client.pentestReportPdf.downloadCustomer({
|
|
47593
|
+
params: { organizationId, pentestId: pentestIdentifier },
|
|
47594
|
+
query: {
|
|
47595
|
+
disposition: "attachment",
|
|
47596
|
+
...artifact === "docx" ? { artifact: "docx" } : {}
|
|
47597
|
+
}
|
|
47598
|
+
})
|
|
47599
|
+
);
|
|
47600
|
+
if (result.status !== SUCCESS_STATUS) {
|
|
47601
|
+
failDeliverable(result.status, result.body, "report", pentestIdentifier, "download");
|
|
47602
|
+
}
|
|
47603
|
+
const { report, downloadUrl } = result.body;
|
|
47604
|
+
const fileName = reportFileName(report, artifact);
|
|
47605
|
+
const { destination, bytes } = await saveDeliverable(downloadUrl, fileName, {
|
|
47606
|
+
...options.out !== void 0 ? { out: options.out } : {},
|
|
47607
|
+
...options.force !== void 0 ? { force: options.force } : {},
|
|
47608
|
+
output
|
|
47609
|
+
});
|
|
47610
|
+
if (output === "json") {
|
|
47611
|
+
printJson({ report, artifact, path: destination, sizeBytes: bytes });
|
|
47612
|
+
return;
|
|
47613
|
+
}
|
|
47614
|
+
if (output === "tsv") {
|
|
47615
|
+
console.log(["documentId", "version", "artifact", "path", "sizeBytes"].join(" "));
|
|
47616
|
+
console.log(
|
|
47617
|
+
[
|
|
47618
|
+
tsvSafe(sanitizeDisplay(report.documentId)),
|
|
47619
|
+
tsvSafe(sanitizeDisplay(report.version)),
|
|
47620
|
+
artifact,
|
|
47621
|
+
tsvSafe(destination),
|
|
47622
|
+
String(bytes)
|
|
47623
|
+
].join(" ")
|
|
47624
|
+
);
|
|
47625
|
+
return;
|
|
47626
|
+
}
|
|
47627
|
+
console.log(
|
|
47628
|
+
colorSuccess(
|
|
47629
|
+
`Saved ${sanitizeDisplay(report.documentId)} (${sanitizeDisplay(report.version)}) to ${destination} (${formatArchiveBytes(bytes)}).`
|
|
47630
|
+
)
|
|
47631
|
+
);
|
|
47632
|
+
}
|
|
47633
|
+
function createReportDownloadCommand() {
|
|
47634
|
+
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(
|
|
47635
|
+
"after",
|
|
47636
|
+
`
|
|
47637
|
+
Downloads the report Trace has published for the engagement: the issued version
|
|
47638
|
+
when one exists, otherwise the current shared draft. The file is named after the
|
|
47639
|
+
report's document ID (for example TR-PT-2026-03-ACME.pdf). Organizations on
|
|
47640
|
+
a partner report template receive a zip carrying the PDF, the Word document, the
|
|
47641
|
+
markdown report, and evidence files.
|
|
47642
|
+
|
|
47643
|
+
Works with a browser session from \`tracecli auth login\` or a Trace API key
|
|
47644
|
+
(TRACE_API_KEY), so it can run unattended in CI.
|
|
47645
|
+
|
|
47646
|
+
Examples:
|
|
47647
|
+
tracecli pentest report download PT-3
|
|
47648
|
+
tracecli pentest report download PT-3 --out ./reports/
|
|
47649
|
+
tracecli pentest report download PT-3 --out ./acme-q3.pdf --force
|
|
47650
|
+
tracecli pentest report download PT-3 --docx
|
|
47651
|
+
tracecli pentest report download PT-3 --output json | jq '.path'`
|
|
47652
|
+
).action(async (pentestId, options) => {
|
|
47653
|
+
await runReportDownload(pentestId, options);
|
|
47654
|
+
});
|
|
47655
|
+
}
|
|
47656
|
+
function createReportCommand() {
|
|
47657
|
+
const report = new Command("report").description("Download the pentest report");
|
|
47658
|
+
report.addCommand(createReportDownloadCommand());
|
|
47659
|
+
return report;
|
|
47660
|
+
}
|
|
47661
|
+
function sortAttestationsNewestFirst(attestations) {
|
|
47662
|
+
return [...attestations].sort((a2, b) => {
|
|
47663
|
+
const delta = new Date(b.createdAt).getTime() - new Date(a2.createdAt).getTime();
|
|
47664
|
+
return delta !== 0 ? delta : a2.attestationId.localeCompare(b.attestationId);
|
|
47665
|
+
});
|
|
47666
|
+
}
|
|
47667
|
+
function renderAttestationLines(attestation) {
|
|
47668
|
+
const counts = attestation.severityCounts;
|
|
47669
|
+
return [
|
|
47670
|
+
` ${sanitizeDisplay(attestation.documentId)} (${sanitizeDisplay(attestation.version)}, ${sanitizeDisplay(attestation.classification)})`,
|
|
47671
|
+
` 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)}`,
|
|
47672
|
+
` ID: ${attestation.attestationId}`,
|
|
47673
|
+
""
|
|
47674
|
+
];
|
|
47675
|
+
}
|
|
47676
|
+
var ATTESTATION_TSV_HEADER = [
|
|
47677
|
+
"attestationId",
|
|
47678
|
+
"documentId",
|
|
47679
|
+
"version",
|
|
47680
|
+
"classification",
|
|
47681
|
+
"issuedDate",
|
|
47682
|
+
"totalFindings",
|
|
47683
|
+
"fileSizeBytes",
|
|
47684
|
+
"createdAt"
|
|
47685
|
+
].join(" ");
|
|
47686
|
+
function attestationTsvRow(attestation) {
|
|
47687
|
+
return [
|
|
47688
|
+
attestation.attestationId,
|
|
47689
|
+
tsvSafe(sanitizeDisplay(attestation.documentId)),
|
|
47690
|
+
tsvSafe(sanitizeDisplay(attestation.version)),
|
|
47691
|
+
tsvSafe(sanitizeDisplay(attestation.classification)),
|
|
47692
|
+
attestation.issuedDate,
|
|
47693
|
+
String(attestation.totalFindings),
|
|
47694
|
+
String(attestation.fileSizeBytes),
|
|
47695
|
+
toIso(attestation.createdAt)
|
|
47696
|
+
].join(" ");
|
|
47697
|
+
}
|
|
47698
|
+
async function fetchAttestations(organizationId, pentestIdentifier) {
|
|
47699
|
+
const result = await authenticatedRequest(
|
|
47700
|
+
(client) => client.pentestAttestation.listCustomerAttestations({
|
|
47701
|
+
params: { organizationId, pentestId: pentestIdentifier }
|
|
47702
|
+
})
|
|
47703
|
+
);
|
|
47704
|
+
if (result.status !== SUCCESS_STATUS) {
|
|
47705
|
+
failDeliverable(result.status, result.body, "attestation", pentestIdentifier, "list");
|
|
47706
|
+
}
|
|
47707
|
+
return sortAttestationsNewestFirst(result.body.attestations);
|
|
47708
|
+
}
|
|
47709
|
+
async function runAttestationList(pentestIdentifier, options) {
|
|
47710
|
+
const output = parseOutput(options.output);
|
|
47711
|
+
requirePentestIdentifier(pentestIdentifier);
|
|
47712
|
+
const organizationId = await resolveOrganizationId();
|
|
47713
|
+
const attestations = await fetchAttestations(organizationId, pentestIdentifier);
|
|
47714
|
+
if (output === "json") {
|
|
47715
|
+
printJson(attestations);
|
|
47716
|
+
return;
|
|
47717
|
+
}
|
|
47718
|
+
if (output === "tsv") {
|
|
47719
|
+
console.log(ATTESTATION_TSV_HEADER);
|
|
47720
|
+
for (const attestation of attestations) {
|
|
47721
|
+
console.log(attestationTsvRow(attestation));
|
|
47722
|
+
}
|
|
47723
|
+
return;
|
|
47724
|
+
}
|
|
47725
|
+
if (attestations.length === 0) {
|
|
47726
|
+
console.log(`No letters of attestation have been shared for pentest ${pentestIdentifier}.`);
|
|
47727
|
+
return;
|
|
47728
|
+
}
|
|
47729
|
+
const header = `Found ${attestations.length} letter${attestations.length === 1 ? "" : "s"} of attestation (newest first):`;
|
|
47730
|
+
await runInteractivePager({
|
|
47731
|
+
items: attestations,
|
|
47732
|
+
header,
|
|
47733
|
+
renderItem: (attestation) => renderAttestationLines(attestation),
|
|
47734
|
+
disabled: options.pager === false
|
|
47735
|
+
});
|
|
47736
|
+
}
|
|
47737
|
+
function createAttestationListCommand() {
|
|
47738
|
+
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(
|
|
47739
|
+
"after",
|
|
47740
|
+
`
|
|
47741
|
+
Works with a browser session from \`tracecli auth login\` or a Trace API key
|
|
47742
|
+
(TRACE_API_KEY).
|
|
47743
|
+
|
|
47744
|
+
Examples:
|
|
47745
|
+
tracecli pentest attestation list PT-3
|
|
47746
|
+
tracecli pentest attestation list PT-3 --output json | jq '.[0].attestationId'
|
|
47747
|
+
tracecli pentest attestation list PT-3 --output tsv | cut -f1,2`
|
|
47748
|
+
).action(async (pentestId, options) => {
|
|
47749
|
+
await runAttestationList(pentestId, options);
|
|
47750
|
+
});
|
|
47751
|
+
}
|
|
47752
|
+
async function runAttestationDownload(pentestIdentifier, options) {
|
|
47753
|
+
const output = parseOutput(options.output);
|
|
47754
|
+
requirePentestIdentifier(pentestIdentifier);
|
|
47755
|
+
if (options.attestation !== void 0 && !isUuid(options.attestation)) {
|
|
47756
|
+
console.error(
|
|
47757
|
+
colorError(
|
|
47758
|
+
`Invalid --attestation '${options.attestation}'. Pass the UUID shown by \`tracecli pentest attestation list ${pentestIdentifier}\`.`
|
|
47759
|
+
)
|
|
47760
|
+
);
|
|
47761
|
+
process.exit(1);
|
|
47762
|
+
}
|
|
47763
|
+
const organizationId = await resolveOrganizationId();
|
|
47764
|
+
let attestationId = options.attestation;
|
|
47765
|
+
if (attestationId === void 0) {
|
|
47766
|
+
const attestations = await fetchAttestations(organizationId, pentestIdentifier);
|
|
47767
|
+
const newest = attestations[0];
|
|
47768
|
+
if (newest === void 0) {
|
|
47769
|
+
console.error(
|
|
47770
|
+
colorError(
|
|
47771
|
+
`No letters of attestation have been shared for pentest ${pentestIdentifier} yet.`
|
|
47772
|
+
)
|
|
47773
|
+
);
|
|
47774
|
+
process.exit(1);
|
|
47775
|
+
}
|
|
47776
|
+
if (attestations.length > 1 && output === "table") {
|
|
47777
|
+
console.error(
|
|
47778
|
+
`Downloading the newest of ${attestations.length} letters (${sanitizeDisplay(newest.documentId)}). Pass --attestation <id> for another.`
|
|
47779
|
+
);
|
|
47780
|
+
}
|
|
47781
|
+
attestationId = newest.attestationId;
|
|
47782
|
+
}
|
|
47783
|
+
const result = await authenticatedRequest(
|
|
47784
|
+
(client) => client.pentestAttestation.downloadCustomerAttestation({
|
|
47785
|
+
params: { organizationId, pentestId: pentestIdentifier, attestationId },
|
|
47786
|
+
query: { disposition: "attachment" }
|
|
47787
|
+
})
|
|
47788
|
+
);
|
|
47789
|
+
if (result.status !== SUCCESS_STATUS) {
|
|
47790
|
+
failDeliverable(result.status, result.body, "attestation", pentestIdentifier, "download");
|
|
47791
|
+
}
|
|
47792
|
+
const { attestation, downloadUrl } = result.body;
|
|
47793
|
+
const fileName = attestationFileName(attestation);
|
|
47794
|
+
const { destination, bytes } = await saveDeliverable(downloadUrl, fileName, {
|
|
47795
|
+
...options.out !== void 0 ? { out: options.out } : {},
|
|
47796
|
+
...options.force !== void 0 ? { force: options.force } : {},
|
|
47797
|
+
output
|
|
47798
|
+
});
|
|
47799
|
+
if (output === "json") {
|
|
47800
|
+
printJson({ attestation, path: destination, sizeBytes: bytes });
|
|
47801
|
+
return;
|
|
47802
|
+
}
|
|
47803
|
+
if (output === "tsv") {
|
|
47804
|
+
console.log(["attestationId", "documentId", "version", "path", "sizeBytes"].join(" "));
|
|
47805
|
+
console.log(
|
|
47806
|
+
[
|
|
47807
|
+
attestation.attestationId,
|
|
47808
|
+
tsvSafe(sanitizeDisplay(attestation.documentId)),
|
|
47809
|
+
tsvSafe(sanitizeDisplay(attestation.version)),
|
|
47810
|
+
tsvSafe(destination),
|
|
47811
|
+
String(bytes)
|
|
47812
|
+
].join(" ")
|
|
47813
|
+
);
|
|
47814
|
+
return;
|
|
47815
|
+
}
|
|
47816
|
+
console.log(
|
|
47817
|
+
colorSuccess(
|
|
47818
|
+
`Saved ${sanitizeDisplay(attestation.documentId)} (${sanitizeDisplay(attestation.version)}) to ${destination} (${formatArchiveBytes(bytes)}).`
|
|
47819
|
+
)
|
|
47820
|
+
);
|
|
47821
|
+
}
|
|
47822
|
+
function createAttestationDownloadCommand() {
|
|
47823
|
+
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(
|
|
47824
|
+
"--attestation <id>",
|
|
47825
|
+
"Attestation UUID from `pentest attestation list` (default: the newest letter)"
|
|
47826
|
+
).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(
|
|
47827
|
+
"after",
|
|
47828
|
+
`
|
|
47829
|
+
The file is named after the letter's document ID (for example
|
|
47830
|
+
TR-LOA-2026-03-ACME-1A2B3C.pdf). Works with a browser session from \`tracecli auth
|
|
47831
|
+
login\` or a Trace API key (TRACE_API_KEY), so it can run unattended in CI.
|
|
47832
|
+
|
|
47833
|
+
Examples:
|
|
47834
|
+
tracecli pentest attestation download PT-3
|
|
47835
|
+
tracecli pentest attestation download PT-3 --out ./compliance/
|
|
47836
|
+
tracecli pentest attestation download PT-3 --attestation 6f1c... --out ./loa.pdf --force
|
|
47837
|
+
tracecli pentest attestation download PT-3 --output json | jq '.path'`
|
|
47838
|
+
).action(async (pentestId, options) => {
|
|
47839
|
+
await runAttestationDownload(pentestId, options);
|
|
47840
|
+
});
|
|
47841
|
+
}
|
|
47842
|
+
function createAttestationCommand() {
|
|
47843
|
+
const attestation = new Command("attestation").alias("loa").description("List and download letters of attestation");
|
|
47844
|
+
attestation.addCommand(createAttestationListCommand());
|
|
47845
|
+
attestation.addCommand(createAttestationDownloadCommand());
|
|
47846
|
+
return attestation;
|
|
47847
|
+
}
|
|
47848
|
+
|
|
47849
|
+
// src/pentest/evidence.ts
|
|
47850
|
+
import { readFileSync as readFileSync6, statSync as statSync4 } from "fs";
|
|
47851
|
+
import { basename as basename4 } from "path";
|
|
47852
|
+
var SECTION_LIST = ValidationEvidenceSectionValues.join(", ");
|
|
47853
|
+
var KIND_LIST = PentestExploitArtifactKindValues.join(", ");
|
|
47854
|
+
function isSection(value) {
|
|
47855
|
+
return ValidationEvidenceSectionValues.includes(value);
|
|
47856
|
+
}
|
|
47857
|
+
function isKind(value) {
|
|
47858
|
+
return PentestExploitArtifactKindValues.includes(value);
|
|
47859
|
+
}
|
|
47860
|
+
function formatBytes2(bytes) {
|
|
47861
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
47862
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
|
|
47863
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
47864
|
+
}
|
|
47865
|
+
async function resolveFindingId(organizationId, pentestId, identifier) {
|
|
47866
|
+
if (isUuid(identifier)) {
|
|
47867
|
+
return identifier;
|
|
47868
|
+
}
|
|
47869
|
+
const numericMatch = identifier.match(/^(?:PTF-)?(\d+)$/i);
|
|
47870
|
+
if (!numericMatch?.[1]) {
|
|
47871
|
+
console.error(
|
|
47872
|
+
colorError(
|
|
47873
|
+
`Invalid finding identifier '${sanitizeDisplay(identifier)}'. Use a finding number (e.g. PTF-7) or UUID.`
|
|
47874
|
+
)
|
|
47875
|
+
);
|
|
47876
|
+
process.exit(1);
|
|
47877
|
+
}
|
|
47878
|
+
const displayId = Number.parseInt(numericMatch[1], 10);
|
|
47879
|
+
const result = await authenticatedRequest(
|
|
47880
|
+
(client) => client.pentestFinding.listFindings({ params: { organizationId, pentestId } })
|
|
47881
|
+
);
|
|
47882
|
+
if (result.status === UNAUTHORIZED_STATUS2) {
|
|
47883
|
+
exitAuthFailed();
|
|
47884
|
+
}
|
|
47885
|
+
if (result.status !== 200) {
|
|
47886
|
+
console.error(colorError(`Failed to list findings (status ${result.status}).`));
|
|
47887
|
+
process.exit(1);
|
|
47888
|
+
}
|
|
47889
|
+
const match = result.body.find((f) => f.displayId === displayId);
|
|
47890
|
+
if (!match || !("pentestFindingId" in match)) {
|
|
47891
|
+
console.error(
|
|
47892
|
+
colorError(
|
|
47893
|
+
`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\`).`
|
|
47894
|
+
)
|
|
47895
|
+
);
|
|
47896
|
+
process.exit(1);
|
|
47897
|
+
}
|
|
47898
|
+
return match.pentestFindingId;
|
|
47899
|
+
}
|
|
47900
|
+
async function runAttachEvidence(options) {
|
|
47901
|
+
if (options.section !== void 0 && !isSection(options.section)) {
|
|
47902
|
+
console.error(colorError(`Invalid --section '${sanitizeDisplay(options.section)}'.`));
|
|
47903
|
+
console.error(`Valid sections: ${SECTION_LIST}`);
|
|
47904
|
+
process.exit(1);
|
|
47905
|
+
}
|
|
47906
|
+
if (options.kind !== void 0 && !isKind(options.kind)) {
|
|
47907
|
+
console.error(colorError(`Invalid --kind '${sanitizeDisplay(options.kind)}'.`));
|
|
47908
|
+
console.error(`Valid kinds: ${KIND_LIST}`);
|
|
47909
|
+
process.exit(1);
|
|
47910
|
+
}
|
|
47911
|
+
let sizeBytes;
|
|
47912
|
+
try {
|
|
47913
|
+
const stats = statSync4(options.file);
|
|
47914
|
+
if (!stats.isFile()) {
|
|
47915
|
+
console.error(colorError(`--file '${sanitizeDisplay(options.file)}' is not a regular file.`));
|
|
47916
|
+
process.exit(1);
|
|
47917
|
+
}
|
|
47918
|
+
sizeBytes = stats.size;
|
|
47919
|
+
} catch {
|
|
47920
|
+
console.error(colorError(`Cannot read --file '${sanitizeDisplay(options.file)}'.`));
|
|
47921
|
+
process.exit(1);
|
|
47922
|
+
}
|
|
47923
|
+
if (sizeBytes === 0) {
|
|
47924
|
+
console.error(colorError(`--file '${sanitizeDisplay(options.file)}' is empty.`));
|
|
47925
|
+
process.exit(1);
|
|
47926
|
+
}
|
|
47927
|
+
if (sizeBytes > MAX_STAFF_EVIDENCE_UPLOAD_BYTES) {
|
|
47928
|
+
console.error(
|
|
47929
|
+
colorError(
|
|
47930
|
+
`File is ${formatBytes2(sizeBytes)}; the limit for one exhibit is ${formatBytes2(MAX_STAFF_EVIDENCE_UPLOAD_BYTES)}.`
|
|
47931
|
+
)
|
|
47932
|
+
);
|
|
47933
|
+
process.exit(1);
|
|
47934
|
+
}
|
|
47935
|
+
const organizationId = await resolveOrganizationId();
|
|
47936
|
+
const pentestId = await resolvePentestId(organizationId, options.pentest);
|
|
47937
|
+
const pentestFindingId = await resolveFindingId(organizationId, pentestId, options.finding);
|
|
47938
|
+
const params = { organizationId, pentestId, pentestFindingId };
|
|
47939
|
+
const filename = basename4(options.file);
|
|
47940
|
+
const slot = await authenticatedRequest(
|
|
47941
|
+
(client) => client.pentestFinding.createFindingEvidenceUpload({
|
|
47942
|
+
params,
|
|
47943
|
+
body: { filename, sizeBytes }
|
|
47944
|
+
})
|
|
47945
|
+
);
|
|
47946
|
+
if (slot.status === UNAUTHORIZED_STATUS2) {
|
|
47947
|
+
exitAuthFailed();
|
|
47948
|
+
}
|
|
47949
|
+
if (slot.status !== 201) {
|
|
47950
|
+
const { message } = readErrorFields(slot.body);
|
|
47951
|
+
console.error(
|
|
47952
|
+
colorError(message ?? `Failed to start the evidence upload (status ${slot.status}).`)
|
|
47953
|
+
);
|
|
47954
|
+
process.exit(1);
|
|
47955
|
+
}
|
|
47956
|
+
const body = readFileSync6(options.file);
|
|
47957
|
+
const put = await fetch(slot.body.uploadUrl, { method: "PUT", body: new Uint8Array(body) });
|
|
47958
|
+
if (!put.ok) {
|
|
47959
|
+
console.error(
|
|
47960
|
+
colorError(
|
|
47961
|
+
`Upload to storage failed (status ${put.status}). The upload link is short-lived \u2014 re-run the command.`
|
|
47962
|
+
)
|
|
47963
|
+
);
|
|
47964
|
+
process.exit(1);
|
|
47965
|
+
}
|
|
47966
|
+
const attached = await authenticatedRequest(
|
|
47967
|
+
(client) => client.pentestFinding.attachFindingEvidence({
|
|
46977
47968
|
params,
|
|
46978
|
-
body:
|
|
47969
|
+
body: {
|
|
47970
|
+
key: slot.body.key,
|
|
47971
|
+
...options.caption !== void 0 ? { caption: options.caption } : {},
|
|
47972
|
+
...options.section !== void 0 && isSection(options.section) ? { section: options.section } : {},
|
|
47973
|
+
...options.kind !== void 0 && isKind(options.kind) ? { kind: options.kind } : {},
|
|
47974
|
+
...options.customerFacing ? { customerFacing: true } : {}
|
|
47975
|
+
}
|
|
46979
47976
|
})
|
|
46980
47977
|
);
|
|
46981
|
-
if (
|
|
46982
|
-
|
|
47978
|
+
if (attached.status === UNAUTHORIZED_STATUS2) {
|
|
47979
|
+
exitAuthFailed();
|
|
47980
|
+
}
|
|
47981
|
+
if (attached.status !== 201) {
|
|
47982
|
+
const { message } = readErrorFields(attached.body);
|
|
47983
|
+
console.error(
|
|
47984
|
+
colorError(message ?? `Failed to attach the evidence (status ${attached.status}).`)
|
|
47985
|
+
);
|
|
47986
|
+
process.exit(1);
|
|
46983
47987
|
}
|
|
46984
47988
|
if (options.output === "json") {
|
|
46985
|
-
|
|
47989
|
+
printJson(attached.body);
|
|
46986
47990
|
return;
|
|
46987
47991
|
}
|
|
46988
|
-
const
|
|
46989
|
-
const
|
|
46990
|
-
console.log(
|
|
47992
|
+
const visibility = attached.body.customerFacing ? "customer-facing" : "staff-internal";
|
|
47993
|
+
const placement = attached.body.section ? ` \xB7 ${attached.body.section}` : "";
|
|
47994
|
+
console.log(
|
|
47995
|
+
colorSuccess(
|
|
47996
|
+
`Attached ${attached.body.kind} \xB7 ${formatBytes2(attached.body.sizeBytes)} \xB7 ${visibility}${placement}`
|
|
47997
|
+
)
|
|
47998
|
+
);
|
|
47999
|
+
console.log(` artifact ${attached.body.artifactId}`);
|
|
48000
|
+
if (attached.body.kind === "HAR") {
|
|
48001
|
+
console.log(" HAR captures stay internal \u2014 they can carry a live session token.");
|
|
48002
|
+
} else if (!attached.body.customerFacing) {
|
|
48003
|
+
console.log(" Publish it from the finding\u2019s evidence panel in the staff console.");
|
|
48004
|
+
}
|
|
46991
48005
|
}
|
|
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(
|
|
48006
|
+
function createAttachEvidenceCommand() {
|
|
48007
|
+
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
48008
|
"after",
|
|
46998
48009
|
`
|
|
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.
|
|
48010
|
+
The file uploads straight to storage, so recordings of any reasonable size work.
|
|
48011
|
+
Videos and HAR captures always land staff-internal \u2014 publish a video with the
|
|
48012
|
+
toggle in the staff console's evidence panel after reviewing it for redaction.
|
|
47011
48013
|
|
|
47012
48014
|
Examples:
|
|
47013
|
-
tracecli pentest findings
|
|
47014
|
-
tracecli pentest findings
|
|
47015
|
-
|
|
47016
|
-
|
|
48015
|
+
tracecli pentest findings attach-evidence --pentest PT-3 --finding 7 --file ./poc.png
|
|
48016
|
+
tracecli pentest findings attach-evidence --pentest PT-3 --finding PTF-7 --file ./poc.mov \\
|
|
48017
|
+
--caption "Cookie survives logout, replayed 40s later" --section proofOfConcept
|
|
48018
|
+
tracecli pentest findings attach-evidence --pentest PT-3 --finding 7 --file ./roster.json \\
|
|
48019
|
+
--caption "Full member roster returned to a non-member" --customer-facing --output json`
|
|
47017
48020
|
).action(async (options) => {
|
|
47018
|
-
await
|
|
48021
|
+
await runAttachEvidence(options);
|
|
47019
48022
|
});
|
|
47020
48023
|
}
|
|
47021
48024
|
|
|
@@ -47136,7 +48139,7 @@ async function runGetFinding(options) {
|
|
|
47136
48139
|
failGetFinding(result.status, result.body);
|
|
47137
48140
|
}
|
|
47138
48141
|
if (options.output === "json") {
|
|
47139
|
-
|
|
48142
|
+
printJson(result.body);
|
|
47140
48143
|
return;
|
|
47141
48144
|
}
|
|
47142
48145
|
process.stdout.write(formatFindingMarkdown(result.body));
|
|
@@ -47377,7 +48380,7 @@ Examples:
|
|
|
47377
48380
|
return;
|
|
47378
48381
|
}
|
|
47379
48382
|
if (isJson) {
|
|
47380
|
-
|
|
48383
|
+
printJson(pentests);
|
|
47381
48384
|
return;
|
|
47382
48385
|
}
|
|
47383
48386
|
if (isTsv) {
|
|
@@ -47526,7 +48529,7 @@ Re-run with: tracecli pentest findings list --pentest ${options.pentest} --detai
|
|
|
47526
48529
|
return;
|
|
47527
48530
|
}
|
|
47528
48531
|
if (isJson) {
|
|
47529
|
-
|
|
48532
|
+
printJson(findings);
|
|
47530
48533
|
return;
|
|
47531
48534
|
}
|
|
47532
48535
|
if (isTsv) {
|
|
@@ -47619,7 +48622,7 @@ async function runFindingsDetailPull(organizationId, options) {
|
|
|
47619
48622
|
}
|
|
47620
48623
|
return { findings: result.body.findings, total: result.body.total };
|
|
47621
48624
|
}, options);
|
|
47622
|
-
|
|
48625
|
+
printJson(matching);
|
|
47623
48626
|
}
|
|
47624
48627
|
function renderPentestFindingLines(finding) {
|
|
47625
48628
|
const sev = formatFindingSeverity(finding.severity);
|
|
@@ -47780,7 +48783,7 @@ function printPentestUpdateTsv(pt) {
|
|
|
47780
48783
|
}
|
|
47781
48784
|
function outputPentestUpdate(pt, output) {
|
|
47782
48785
|
if (output === "json") {
|
|
47783
|
-
|
|
48786
|
+
printJson(pt);
|
|
47784
48787
|
return;
|
|
47785
48788
|
}
|
|
47786
48789
|
if (output === "tsv") {
|
|
@@ -48130,12 +49133,14 @@ function createPentestCommand() {
|
|
|
48130
49133
|
pentest.addCommand(createUpdateCommand4());
|
|
48131
49134
|
pentest.addCommand(createQuestionnaireCommand());
|
|
48132
49135
|
pentest.addCommand(createFindingsCommand());
|
|
49136
|
+
pentest.addCommand(createReportCommand());
|
|
49137
|
+
pentest.addCommand(createAttestationCommand());
|
|
48133
49138
|
return pentest;
|
|
48134
49139
|
}
|
|
48135
49140
|
|
|
48136
49141
|
// src/update/commands.ts
|
|
48137
49142
|
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
|
|
48138
|
-
import { dirname as dirname3, join as
|
|
49143
|
+
import { dirname as dirname3, join as join7 } from "path";
|
|
48139
49144
|
import { createInterface as createInterface4 } from "readline/promises";
|
|
48140
49145
|
var UPDATE_CACHE_FILE = "update-check.json";
|
|
48141
49146
|
var UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -48144,7 +49149,7 @@ var DISABLE_UPDATE_NOTIFIER_ENV = "TRACECLI_DISABLE_UPDATE_NOTIFIER";
|
|
|
48144
49149
|
var REGISTRY_URL_ENV = "TRACECLI_NPM_REGISTRY_URL";
|
|
48145
49150
|
var noticePrinted = false;
|
|
48146
49151
|
function getUpdateCheckCachePath() {
|
|
48147
|
-
return
|
|
49152
|
+
return join7(getTraceDir(), UPDATE_CACHE_FILE);
|
|
48148
49153
|
}
|
|
48149
49154
|
function readUpdateCheckCache() {
|
|
48150
49155
|
const cachePath = getUpdateCheckCachePath();
|
|
@@ -48518,153 +49523,8 @@ Examples:
|
|
|
48518
49523
|
|
|
48519
49524
|
// src/upload/download.ts
|
|
48520
49525
|
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
|
|
49526
|
+
import { open as open3, rename as rename2, rm as rm2, stat as stat2 } from "fs/promises";
|
|
49527
|
+
import { basename as basename5, join as join8, resolve as resolve2 } from "path";
|
|
48668
49528
|
var CodeUploadDownloadError = class extends Error {
|
|
48669
49529
|
constructor(message) {
|
|
48670
49530
|
super(message);
|
|
@@ -48674,7 +49534,7 @@ var CodeUploadDownloadError = class extends Error {
|
|
|
48674
49534
|
function safeDownloadFilename(upload) {
|
|
48675
49535
|
const extension = requiredExtensionForKind(upload.artifactKind);
|
|
48676
49536
|
const fallback = `app${extension}`;
|
|
48677
|
-
const stripped = Array.from(
|
|
49537
|
+
const stripped = Array.from(basename5(upload.originalFilename.replace(/\\/g, "/")), (char) => {
|
|
48678
49538
|
const code = char.codePointAt(0) ?? 0;
|
|
48679
49539
|
return code <= 31 || code === 127 ? "_" : char;
|
|
48680
49540
|
}).join("").trim();
|
|
@@ -48683,18 +49543,18 @@ function safeDownloadFilename(upload) {
|
|
|
48683
49543
|
}
|
|
48684
49544
|
async function resolveDownloadDestination(upload, outOption) {
|
|
48685
49545
|
const filename = safeDownloadFilename(upload);
|
|
48686
|
-
if (outOption === void 0) return
|
|
48687
|
-
const target =
|
|
49546
|
+
if (outOption === void 0) return resolve2(process.cwd(), filename);
|
|
49547
|
+
const target = resolve2(outOption);
|
|
48688
49548
|
try {
|
|
48689
|
-
const info = await
|
|
48690
|
-
if (info.isDirectory()) return
|
|
49549
|
+
const info = await stat2(target);
|
|
49550
|
+
if (info.isDirectory()) return join8(target, filename);
|
|
48691
49551
|
} catch {
|
|
48692
49552
|
}
|
|
48693
49553
|
return target;
|
|
48694
49554
|
}
|
|
48695
|
-
async function
|
|
49555
|
+
async function pathExists2(path) {
|
|
48696
49556
|
try {
|
|
48697
|
-
await
|
|
49557
|
+
await stat2(path);
|
|
48698
49558
|
return true;
|
|
48699
49559
|
} catch {
|
|
48700
49560
|
return false;
|
|
@@ -48726,8 +49586,8 @@ async function downloadCodeUploadToFile(upload, downloadUrl, destination, option
|
|
|
48726
49586
|
);
|
|
48727
49587
|
}
|
|
48728
49588
|
}
|
|
48729
|
-
await
|
|
48730
|
-
const file2 = await
|
|
49589
|
+
await rm2(partialPath, { force: true });
|
|
49590
|
+
const file2 = await open3(partialPath, "wx");
|
|
48731
49591
|
const hash2 = createHash2("sha256");
|
|
48732
49592
|
const reader = response.body.getReader();
|
|
48733
49593
|
let downloadedBytes = 0;
|
|
@@ -48744,7 +49604,7 @@ async function downloadCodeUploadToFile(upload, downloadUrl, destination, option
|
|
|
48744
49604
|
);
|
|
48745
49605
|
}
|
|
48746
49606
|
hash2.update(chunk.value);
|
|
48747
|
-
await
|
|
49607
|
+
await writeAll2(file2, chunk.value);
|
|
48748
49608
|
options.onProgress?.(progress(downloadedBytes, upload.sizeBytes));
|
|
48749
49609
|
}
|
|
48750
49610
|
await file2.close();
|
|
@@ -48759,10 +49619,10 @@ async function downloadCodeUploadToFile(upload, downloadUrl, destination, option
|
|
|
48759
49619
|
"Downloaded bytes do not match the SHA-256 recorded at upload time."
|
|
48760
49620
|
);
|
|
48761
49621
|
}
|
|
48762
|
-
await
|
|
49622
|
+
await rename2(partialPath, destination);
|
|
48763
49623
|
} catch (error48) {
|
|
48764
49624
|
await file2.close().catch(() => void 0);
|
|
48765
|
-
await
|
|
49625
|
+
await rm2(partialPath, { force: true });
|
|
48766
49626
|
if (error48 instanceof CodeUploadDownloadError) throw error48;
|
|
48767
49627
|
const detail = error48 instanceof Error ? error48.message : "unknown error";
|
|
48768
49628
|
throw new CodeUploadDownloadError(`Download from storage failed: ${detail}.`);
|
|
@@ -48775,7 +49635,7 @@ function progress(downloadedBytes, totalBytes) {
|
|
|
48775
49635
|
fraction: totalBytes === 0 ? 1 : downloadedBytes / totalBytes
|
|
48776
49636
|
};
|
|
48777
49637
|
}
|
|
48778
|
-
async function
|
|
49638
|
+
async function writeAll2(file2, bytes) {
|
|
48779
49639
|
let offset = 0;
|
|
48780
49640
|
while (offset < bytes.byteLength) {
|
|
48781
49641
|
const { bytesWritten } = await file2.write(bytes, offset, bytes.byteLength - offset);
|
|
@@ -48911,13 +49771,13 @@ function progress2(uploadedBytes, totalBytes) {
|
|
|
48911
49771
|
};
|
|
48912
49772
|
}
|
|
48913
49773
|
function defaultWait(milliseconds) {
|
|
48914
|
-
return new Promise((
|
|
49774
|
+
return new Promise((resolve3) => setTimeout(resolve3, milliseconds));
|
|
48915
49775
|
}
|
|
48916
49776
|
|
|
48917
49777
|
// src/upload/commands.ts
|
|
48918
49778
|
var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["READY", "REJECTED", "FAILED", "DELETING"]);
|
|
48919
49779
|
var DEFAULT_RUNTIME = {
|
|
48920
|
-
wait: (milliseconds) => new Promise((
|
|
49780
|
+
wait: (milliseconds) => new Promise((resolve3) => setTimeout(resolve3, milliseconds)),
|
|
48921
49781
|
pollIntervalMs: 3e3,
|
|
48922
49782
|
fetch: (input, init) => globalThis.fetch(input, init)
|
|
48923
49783
|
};
|
|
@@ -49157,7 +50017,7 @@ function printUploadsTsv(uploads) {
|
|
|
49157
50017
|
}
|
|
49158
50018
|
function outputUpload(upload, output) {
|
|
49159
50019
|
if (output === "json") {
|
|
49160
|
-
|
|
50020
|
+
printJson(upload);
|
|
49161
50021
|
return;
|
|
49162
50022
|
}
|
|
49163
50023
|
if (output === "tsv") {
|
|
@@ -49173,14 +50033,14 @@ function writeProgress(progress3) {
|
|
|
49173
50033
|
`\rUploading ${pct}% (${formatArchiveBytes(progress3.uploadedBytes)} / ${formatArchiveBytes(progress3.totalBytes)})`
|
|
49174
50034
|
);
|
|
49175
50035
|
}
|
|
49176
|
-
function
|
|
50036
|
+
function writeDownloadProgress2(progress3) {
|
|
49177
50037
|
if (!process.stderr.isTTY) return;
|
|
49178
50038
|
const pct = Math.floor(progress3.fraction * 100);
|
|
49179
50039
|
process.stderr.write(
|
|
49180
50040
|
`\rDownloading ${pct}% (${formatArchiveBytes(progress3.downloadedBytes)} / ${formatArchiveBytes(progress3.totalBytes)})`
|
|
49181
50041
|
);
|
|
49182
50042
|
}
|
|
49183
|
-
function
|
|
50043
|
+
function finishProgress2() {
|
|
49184
50044
|
if (process.stderr.isTTY) process.stderr.write("\n");
|
|
49185
50045
|
}
|
|
49186
50046
|
async function listUploads(organizationId, query = {}) {
|
|
@@ -49310,9 +50170,9 @@ async function runTransfer(filePath, options, runtime) {
|
|
|
49310
50170
|
refreshParts: () => refreshRemainingParts(organizationId, codeUploadId)
|
|
49311
50171
|
}
|
|
49312
50172
|
);
|
|
49313
|
-
|
|
50173
|
+
finishProgress2();
|
|
49314
50174
|
} catch (error48) {
|
|
49315
|
-
|
|
50175
|
+
finishProgress2();
|
|
49316
50176
|
const detail = error48 instanceof Error ? error48.message : "Upload to storage failed.";
|
|
49317
50177
|
console.error(colorError(detail));
|
|
49318
50178
|
console.error(resumeHint(codeUploadId, filePath));
|
|
@@ -49425,9 +50285,9 @@ async function runResume(codeUploadIdArg, filePath, options, runtime) {
|
|
|
49425
50285
|
refreshParts: () => refreshRemainingParts(organizationId, codeUploadId)
|
|
49426
50286
|
}
|
|
49427
50287
|
);
|
|
49428
|
-
|
|
50288
|
+
finishProgress2();
|
|
49429
50289
|
} catch (error48) {
|
|
49430
|
-
|
|
50290
|
+
finishProgress2();
|
|
49431
50291
|
const detail = error48 instanceof Error ? error48.message : "Upload to storage failed.";
|
|
49432
50292
|
console.error(colorError(detail));
|
|
49433
50293
|
console.error(resumeHint(codeUploadId, filePath));
|
|
@@ -49465,7 +50325,7 @@ async function runList5(options) {
|
|
|
49465
50325
|
...applicationId !== void 0 ? { applicationId } : {}
|
|
49466
50326
|
});
|
|
49467
50327
|
if (output === "json") {
|
|
49468
|
-
|
|
50328
|
+
printJson({ uploads });
|
|
49469
50329
|
return;
|
|
49470
50330
|
}
|
|
49471
50331
|
if (output === "tsv") {
|
|
@@ -49505,7 +50365,7 @@ async function runDownload(codeUploadIdArg, options, runtime) {
|
|
|
49505
50365
|
}
|
|
49506
50366
|
const { upload, downloadUrl } = result.body;
|
|
49507
50367
|
const destination = await resolveDownloadDestination(upload, options.out);
|
|
49508
|
-
if (!options.force && await
|
|
50368
|
+
if (!options.force && await pathExists2(destination)) {
|
|
49509
50369
|
console.error(
|
|
49510
50370
|
colorError(`${destination} already exists. Pass --force to overwrite it or --out <path>.`)
|
|
49511
50371
|
);
|
|
@@ -49519,28 +50379,22 @@ async function runDownload(codeUploadIdArg, options, runtime) {
|
|
|
49519
50379
|
try {
|
|
49520
50380
|
await downloadCodeUploadToFile(upload, downloadUrl, destination, {
|
|
49521
50381
|
fetch: runtime.fetch,
|
|
49522
|
-
onProgress:
|
|
50382
|
+
onProgress: writeDownloadProgress2
|
|
49523
50383
|
});
|
|
49524
|
-
|
|
50384
|
+
finishProgress2();
|
|
49525
50385
|
} catch (error48) {
|
|
49526
|
-
|
|
50386
|
+
finishProgress2();
|
|
49527
50387
|
const detail = error48 instanceof Error ? error48.message : "Download from storage failed.";
|
|
49528
50388
|
console.error(colorError(detail));
|
|
49529
50389
|
process.exit(1);
|
|
49530
50390
|
}
|
|
49531
50391
|
if (output === "json") {
|
|
49532
|
-
|
|
49533
|
-
|
|
49534
|
-
|
|
49535
|
-
|
|
49536
|
-
|
|
49537
|
-
|
|
49538
|
-
archiveSha256: upload.archiveSha256
|
|
49539
|
-
},
|
|
49540
|
-
null,
|
|
49541
|
-
2
|
|
49542
|
-
)
|
|
49543
|
-
);
|
|
50392
|
+
printJson({
|
|
50393
|
+
upload,
|
|
50394
|
+
path: destination,
|
|
50395
|
+
sizeBytes: upload.sizeBytes,
|
|
50396
|
+
archiveSha256: upload.archiveSha256
|
|
50397
|
+
});
|
|
49544
50398
|
return;
|
|
49545
50399
|
}
|
|
49546
50400
|
if (output === "tsv") {
|
|
@@ -49618,7 +50472,7 @@ async function runLink(options) {
|
|
|
49618
50472
|
failWriteOp5("link", result.status, result.body);
|
|
49619
50473
|
}
|
|
49620
50474
|
if (output === "json") {
|
|
49621
|
-
|
|
50475
|
+
printJson(result.body);
|
|
49622
50476
|
return;
|
|
49623
50477
|
}
|
|
49624
50478
|
if (output === "tsv") {
|
|
@@ -49965,7 +50819,7 @@ Run \`tracecli vulnerabilities list\` to see available vulnerabilities.`
|
|
|
49965
50819
|
process.exit(1);
|
|
49966
50820
|
}
|
|
49967
50821
|
if (options.output === "json") {
|
|
49968
|
-
|
|
50822
|
+
printJson(result.body);
|
|
49969
50823
|
return;
|
|
49970
50824
|
}
|
|
49971
50825
|
if (options.output === "tsv") {
|
|
@@ -50089,7 +50943,7 @@ Examples:
|
|
|
50089
50943
|
return;
|
|
50090
50944
|
}
|
|
50091
50945
|
if (isJson) {
|
|
50092
|
-
|
|
50946
|
+
printJson(items);
|
|
50093
50947
|
} else if (isTsv) {
|
|
50094
50948
|
printVulnerabilitiesTsv(items);
|
|
50095
50949
|
} else {
|