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/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 = ["ENGAGEMENT_BRIEF", "VERIFY_APPLICATION_SETUP"];
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: FindingProseSchema,
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 v3.1 base vector for the demonstrated impact
26256
- * (PROVEN-only). The api derives and persists the canonical vector + base
26257
- * score AND the finding severity (from the score's band) server-side; an
26258
- * unparseable explicit vector is rejected (400). The score is NEVER
26259
- * client-supplied. Absent → both cvss columns null and severity falls back
26260
- * to the agent's `severity` field (informational findings carry no vector).
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. Server-sorted: measured first by
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
- /** Resolved branch the measurement is keyed on (scanningBranch ?? default). */
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 — the re-measure `all` scope size. */
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) or schema validation.
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((resolve2) => setTimeout(resolve2, ms));
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 = (open3, close, parent) => {
39074
+ var createStyler = (open4, close, parent) => {
38923
39075
  let openAll;
38924
39076
  let closeAll;
38925
39077
  if (parent === void 0) {
38926
- openAll = open3;
39078
+ openAll = open4;
38927
39079
  closeAll = close;
38928
39080
  } else {
38929
- openAll = parent.openAll + open3;
39081
+ openAll = parent.openAll + open4;
38930
39082
  closeAll = close + parent.closeAll;
38931
39083
  }
38932
39084
  return {
38933
- open: open3,
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("repositoryUrl platform status githubDefaultBranch scanningBranch language");
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, toRepositoryListFilterOptions(options));
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
- console.log(JSON.stringify(displayedRepositories, null, 2));
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
- console.log(JSON.stringify(displayedRepos, null, 2));
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
- console.log(JSON.stringify(allResults, null, 2));
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
- console.log(JSON.stringify(app, null, 2));
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
- console.log(JSON.stringify(apps, null, 2));
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
- console.log(JSON.stringify(links, null, 2));
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
- console.log(JSON.stringify(org, null, 2));
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
- console.log(JSON.stringify(info, null, 2));
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
- console.log(JSON.stringify(items, null, 2));
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
- console.log(
43553
- JSON.stringify(
43554
- {
43555
- ...active,
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: repoDisplayName(repo.repositoryUrl),
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
- (repo) => `${repoDisplayName(repo.repositoryUrl)} ${repo.billableSloc === null ? "" : repo.billableSloc}`
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
- console.log(JSON.stringify(codeSize, null, 2));
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
- console.log(JSON.stringify(inbox, null, 2));
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
- console.log(JSON.stringify({ inboxes, quota }, null, 2));
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
- console.log(JSON.stringify(result.body, null, 2));
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
- console.log(JSON.stringify(result.body, null, 2));
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
- console.log(JSON.stringify(cred, null, 2));
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
- console.log(JSON.stringify(creds, null, 2));
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
- console.log(JSON.stringify(result.body, null, 2));
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
- console.log(JSON.stringify(env2, null, 2));
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
- console.log(JSON.stringify(envs, null, 2));
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
- console.log(JSON.stringify(envs, null, 2));
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
- console.log(JSON.stringify(result.body, null, 2));
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("--cvss-vector <vector>", "CVSS v3 vector string").option("--cvss-score <n>", "CVSS base score (0\u201310)").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) => {
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/evidence.ts
46666
- import { readFileSync as readFileSync6, statSync as statSync3 } from "fs";
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 SECTION_LIST = ValidationEvidenceSectionValues.join(", ");
46669
- var KIND_LIST = PentestExploitArtifactKindValues.join(", ");
46670
- function isSection(value) {
46671
- return ValidationEvidenceSectionValues.includes(value);
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 isKind(value) {
46674
- return PentestExploitArtifactKindValues.includes(value);
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 formatBytes2(bytes) {
46677
- if (bytes < 1024) return `${bytes} B`;
46678
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
46679
- return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
47070
+ function requiredExtensionForKind(kind) {
47071
+ if (kind === "ANDROID_APK") return ".apk";
47072
+ if (kind === "IOS_IPA") return ".ipa";
47073
+ return ".zip";
46680
47074
  }
46681
- async function resolveFindingId(organizationId, pentestId, identifier) {
46682
- if (isUuid(identifier)) {
46683
- return identifier;
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
- const numericMatch = identifier.match(/^(?:PTF-)?(\d+)$/i);
46686
- if (!numericMatch?.[1]) {
46687
- console.error(
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
- if (result.status !== 200) {
46702
- console.error(colorError(`Failed to list findings (status ${result.status}).`));
46703
- process.exit(1);
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
- const match = result.body.find((f) => f.displayId === displayId);
46706
- if (!match || !("pentestFindingId" in match)) {
46707
- console.error(
46708
- colorError(
46709
- `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\`).`
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
- return match.pentestFindingId;
46715
- }
46716
- async function runAttachEvidence(options) {
46717
- if (options.section !== void 0 && !isSection(options.section)) {
46718
- console.error(colorError(`Invalid --section '${sanitizeDisplay(options.section)}'.`));
46719
- console.error(`Valid sections: ${SECTION_LIST}`);
46720
- process.exit(1);
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
- if (options.kind !== void 0 && !isKind(options.kind)) {
46723
- console.error(colorError(`Invalid --kind '${sanitizeDisplay(options.kind)}'.`));
46724
- console.error(`Valid kinds: ${KIND_LIST}`);
46725
- process.exit(1);
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(options.file);
47134
+ const stats = statSync3(filePath);
46730
47135
  if (!stats.isFile()) {
46731
- console.error(colorError(`--file '${sanitizeDisplay(options.file)}' is not a regular file.`));
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
- console.error(colorError(`Cannot read --file '${sanitizeDisplay(options.file)}'.`));
46737
- process.exit(1);
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
- const body = readFileSync6(options.file);
46773
- const put = await fetch(slot.body.uploadUrl, { method: "PUT", body: new Uint8Array(body) });
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
- const attached = await authenticatedRequest(
46783
- (client) => client.pentestFinding.attachFindingEvidence({
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
- if (attached.status !== 201) {
46798
- const { message } = readErrorFields(attached.body);
46799
- console.error(
46800
- colorError(message ?? `Failed to attach the evidence (status ${attached.status}).`)
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
- if (options.output === "json") {
46805
- console.log(JSON.stringify(attached.body, null, 2));
46806
- return;
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 visibility = attached.body.customerFacing ? "customer-facing" : "staff-internal";
46809
- const placement = attached.body.section ? ` \xB7 ${attached.body.section}` : "";
46810
- console.log(
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
- function createAttachEvidenceCommand() {
46823
- 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(
46824
- "after",
46825
- `
46826
- The file uploads straight to storage, so recordings of any reasonable size work.
46827
- Videos and HAR captures always land staff-internal \u2014 publish a video with the
46828
- toggle in the staff console's evidence panel after reviewing it for redaction.
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: note !== void 0 ? { note } : {}
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 (result.status !== 200) {
46982
- failStatusUpdate(result.status, result.body, viaApiKey);
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
- console.log(JSON.stringify(result.body, null, 2));
47989
+ printJson(attached.body);
46986
47990
  return;
46987
47991
  }
46988
- const displayId = result.body.displayId;
46989
- const status = sanitizeDisplay(result.body.status);
46990
- console.log(colorSuccess(`PTF-${displayId} is now ${status}`));
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 createStatusUpdateCommand() {
46993
- 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(
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
- Customer-facing status writes \u2014 the same transitions as the dashboard. An agent
47000
- can mark a finding in progress when it starts the fix, then remediated when the
47001
- fix is ready for Trace to re-test. Leaving the finding IN_PROGRESS (and not
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 status-update --pentest PT-3 --finding 7 --status IN_PROGRESS
47014
- tracecli pentest findings status-update --pentest PT-3 --finding PTF-7 --status REMEDIATED
47015
- tracecli pentest findings status-update --pentest PT-3 --finding 7 --status REMEDIATED \\
47016
- --note "Fixed in PR 412" --output json`
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 runStatusUpdate(options);
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
- console.log(JSON.stringify(result.body, null, 2));
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
- console.log(JSON.stringify(pentests, null, 2));
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
- console.log(JSON.stringify(findings, null, 2));
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
- console.log(JSON.stringify(matching, null, 2));
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
- console.log(JSON.stringify(pt, null, 2));
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 join6 } from "path";
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 join6(getTraceDir(), UPDATE_CACHE_FILE);
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 open2, rename, rm, stat } from "fs/promises";
48522
- import { basename as basename4, join as join7, resolve } from "path";
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(basename4(upload.originalFilename.replace(/\\/g, "/")), (char) => {
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 resolve(process.cwd(), filename);
48687
- const target = resolve(outOption);
49546
+ if (outOption === void 0) return resolve2(process.cwd(), filename);
49547
+ const target = resolve2(outOption);
48688
49548
  try {
48689
- const info = await stat(target);
48690
- if (info.isDirectory()) return join7(target, filename);
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 pathExists(path) {
49555
+ async function pathExists2(path) {
48696
49556
  try {
48697
- await stat(path);
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 rm(partialPath, { force: true });
48730
- const file2 = await open2(partialPath, "wx");
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 writeAll(file2, chunk.value);
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 rename(partialPath, destination);
49622
+ await rename2(partialPath, destination);
48763
49623
  } catch (error48) {
48764
49624
  await file2.close().catch(() => void 0);
48765
- await rm(partialPath, { force: true });
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 writeAll(file2, bytes) {
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((resolve2) => setTimeout(resolve2, milliseconds));
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((resolve2) => setTimeout(resolve2, milliseconds)),
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
- console.log(JSON.stringify(upload, null, 2));
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 writeDownloadProgress(progress3) {
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 finishProgress() {
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
- finishProgress();
50173
+ finishProgress2();
49314
50174
  } catch (error48) {
49315
- finishProgress();
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
- finishProgress();
50288
+ finishProgress2();
49429
50289
  } catch (error48) {
49430
- finishProgress();
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
- console.log(JSON.stringify({ uploads }, null, 2));
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 pathExists(destination)) {
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: writeDownloadProgress
50382
+ onProgress: writeDownloadProgress2
49523
50383
  });
49524
- finishProgress();
50384
+ finishProgress2();
49525
50385
  } catch (error48) {
49526
- finishProgress();
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
- console.log(
49533
- JSON.stringify(
49534
- {
49535
- upload,
49536
- path: destination,
49537
- sizeBytes: upload.sizeBytes,
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
- console.log(JSON.stringify(result.body, null, 2));
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
- console.log(JSON.stringify(result.body, null, 2));
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
- console.log(JSON.stringify(items, null, 2));
50946
+ printJson(items);
50093
50947
  } else if (isTsv) {
50094
50948
  printVulnerabilitiesTsv(items);
50095
50949
  } else {