securewithtrace 0.43.0 → 0.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3683,6 +3683,22 @@ function linkRolesDisplay(roles, role, roleLabel) {
3683
3683
  // ../../packages/core/src/application/environment-urls.ts
3684
3684
  var ENVIRONMENT_ADDITIONAL_URLS_MAX = 20;
3685
3685
 
3686
+ // ../../packages/core/src/pentest/attack-chain-status.ts
3687
+ var ATTACK_CHAIN_STATUS_VALUES = ["OPEN", "REMEDIATED", "VERIFIED_FIXED"];
3688
+ var ATTACK_CHAIN_STEP_STATE_VALUES = ATTACK_CHAIN_STATUS_VALUES;
3689
+
3690
+ // ../../packages/core/src/pentest/finding-status.ts
3691
+ var PENTEST_FINDING_STATUS_VALUES = [
3692
+ "OPEN",
3693
+ "IN_PROGRESS",
3694
+ "CONTESTED",
3695
+ "REMEDIATED",
3696
+ "VERIFIED_FIXED",
3697
+ "VERIFIED_OPEN",
3698
+ "ACCEPTED_WITH_CONTROLS",
3699
+ "DISMISSED"
3700
+ ];
3701
+
3686
3702
  // ../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/classic/external.js
3687
3703
  var external_exports = {};
3688
3704
  __export(external_exports, {
@@ -21884,16 +21900,7 @@ var PentestStatusValues = ["SCOPING", "IN_PROGRESS", "REMEDIATION", "CLOSED"];
21884
21900
  var PentestStatus = external_exports.enum(PentestStatusValues);
21885
21901
  var PentestTestTypeValues = ["BLACK_BOX", "GREY_BOX", "WHITE_BOX"];
21886
21902
  var PentestTestType = external_exports.enum(PentestTestTypeValues);
21887
- var PentestFindingStatusValues = [
21888
- "OPEN",
21889
- "IN_PROGRESS",
21890
- "CONTESTED",
21891
- "REMEDIATED",
21892
- "VERIFIED_FIXED",
21893
- "VERIFIED_OPEN",
21894
- "ACCEPTED_WITH_CONTROLS",
21895
- "DISMISSED"
21896
- ];
21903
+ var PentestFindingStatusValues = PENTEST_FINDING_STATUS_VALUES;
21897
21904
  var PentestFindingStatus = external_exports.enum(PentestFindingStatusValues);
21898
21905
  var PentestFindingDismissReasonValues = [
21899
21906
  "FALSE_POSITIVE",
@@ -22633,7 +22640,11 @@ var agentSessionContract = c2.router({
22633
22640
  });
22634
22641
 
22635
22642
  // ../../packages/api-contracts/src/schemas/api/v1/agent-task.api.ts
22636
- 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
+ ];
22637
22648
  var AgentTaskTypeSchema = external_exports.enum(AGENT_TASK_TYPES);
22638
22649
  var TRIGGERABLE_AGENT_TASK_TYPES = ["ENGAGEMENT_BRIEF"];
22639
22650
  var TriggerableAgentTaskTypeSchema = external_exports.enum(TRIGGERABLE_AGENT_TASK_TYPES);
@@ -22694,6 +22705,7 @@ var AgentTaskResponseSchema = external_exports.object({
22694
22705
  status: AgentTaskStatusSchema,
22695
22706
  subjectPentestId: external_exports.uuid().nullable(),
22696
22707
  subjectApplicationId: external_exports.uuid().nullable(),
22708
+ subjectVulnerabilityId: external_exports.uuid().nullable(),
22697
22709
  capabilities: external_exports.array(external_exports.string()),
22698
22710
  /**
22699
22711
  * Null until the worker claims the task and records what it actually ran —
@@ -22846,6 +22858,78 @@ var ListAgentTasksQuerySchema = external_exports.object({
22846
22858
  });
22847
22859
  var ListAgentTasksResponseSchema = external_exports.array(AgentTaskResponseSchema);
22848
22860
 
22861
+ // ../../packages/api-contracts/src/schemas/api/v1/inbox.api.ts
22862
+ var InboxLabelSchema = external_exports.string().regex(
22863
+ /^[a-z][a-z0-9-]{0,39}$/,
22864
+ "Inbox label must be lowercase letters/digits/dashes, starting with a letter, at most 40 characters"
22865
+ );
22866
+ var MAX_INBOX_LABEL_LENGTH = 40;
22867
+ var MAX_LOCAL_PART_LENGTH = 64;
22868
+ var MAX_INBOX_ADDRESS_PREFIX_LENGTH = MAX_LOCAL_PART_LENGTH - MAX_INBOX_LABEL_LENGTH - 1;
22869
+ var MANAGED_INBOX_DOMAINS = [
22870
+ "pentest.securewithtrace.com",
22871
+ "pentest.rmrflabs.com"
22872
+ ];
22873
+ var ManagedInboxDomainSchema = external_exports.enum(MANAGED_INBOX_DOMAINS);
22874
+ var DEFAULT_MANAGED_INBOX_DOMAIN = MANAGED_INBOX_DOMAINS[0];
22875
+ var ManagedInboxResponseSchema = external_exports.object({
22876
+ /** Full derived address, e.g. `acme-qa-admin@pentest.securewithtrace.com`. */
22877
+ address: external_exports.email(),
22878
+ label: external_exports.string(),
22879
+ createdAt: external_exports.date()
22880
+ });
22881
+ var ListManagedInboxesResponseSchema = external_exports.object({
22882
+ inboxes: external_exports.array(ManagedInboxResponseSchema),
22883
+ quota: external_exports.object({
22884
+ used: external_exports.number().int().nonnegative(),
22885
+ limit: external_exports.number().int().positive()
22886
+ })
22887
+ });
22888
+ var CreateManagedInboxRequestSchema = external_exports.object({
22889
+ label: InboxLabelSchema
22890
+ }).strict();
22891
+ var ManagedInboxLabelParamsSchema = OrgIdParamsSchema.extend({
22892
+ label: external_exports.string().min(1).max(64)
22893
+ });
22894
+ var ManagedInboxMessageParamsSchema = ManagedInboxLabelParamsSchema.extend({
22895
+ messageId: external_exports.string().min(1).max(500)
22896
+ });
22897
+ var ListInboxMessagesQuerySchema = external_exports.object({
22898
+ /** Max messages, newest first. The provider may clamp further. */
22899
+ limit: external_exports.coerce.number().int().positive().max(50).optional(),
22900
+ /** Only messages received strictly after this instant. */
22901
+ since: external_exports.coerce.date().optional()
22902
+ }).strict();
22903
+ var ManagedInboxMessageSummarySchema = external_exports.object({
22904
+ messageId: external_exports.string().min(1).max(500),
22905
+ from: external_exports.string().max(500),
22906
+ /** Recipients as the provider returned them (display-name form allowed). */
22907
+ to: external_exports.array(external_exports.string().max(500)),
22908
+ subject: external_exports.string().max(1e3).nullable(),
22909
+ preview: external_exports.string().max(2e3).nullable(),
22910
+ receivedAt: external_exports.coerce.date(),
22911
+ unread: external_exports.boolean()
22912
+ });
22913
+ var ListInboxMessagesResponseSchema = external_exports.object({
22914
+ address: external_exports.email(),
22915
+ label: external_exports.string(),
22916
+ messages: external_exports.array(ManagedInboxMessageSummarySchema)
22917
+ });
22918
+ var GetInboxMessageResponseSchema = external_exports.object({
22919
+ address: external_exports.email(),
22920
+ label: external_exports.string(),
22921
+ message: ManagedInboxMessageSummarySchema.extend({
22922
+ body: external_exports.string(),
22923
+ truncated: external_exports.boolean(),
22924
+ /**
22925
+ * http(s) links extracted from the message (hrefs first, then inline
22926
+ * text URLs). Button-style mail often carries the magic link only in
22927
+ * `href`, which a tag-stripped body can still miss if extraction fails.
22928
+ */
22929
+ links: external_exports.array(external_exports.string())
22930
+ })
22931
+ });
22932
+
22849
22933
  // ../../packages/api-contracts/src/schemas/api/v1/pentest-attack-chain.api.ts
22850
22934
  var PentestAttackChainParamsSchema = OrgIdParamsSchema.extend({
22851
22935
  pentestId: external_exports.uuid()
@@ -22876,6 +22960,23 @@ var UpdatePentestAttackChainRequestSchema = CreatePentestAttackChainRequestSchem
22876
22960
  var PublishPentestAttackChainRequestSchema = external_exports.object({
22877
22961
  expectedUpdatedAt: external_exports.coerce.date()
22878
22962
  });
22963
+ var PublishAllAttackChainsRequestSchema = external_exports.object({
22964
+ attackChainIds: external_exports.array(external_exports.uuid()).max(1e3).optional()
22965
+ });
22966
+ var PublishedAttackChainSchema = external_exports.object({
22967
+ attackChainId: external_exports.uuid(),
22968
+ displayId: external_exports.int()
22969
+ });
22970
+ var SkippedAttackChainReason = external_exports.enum(["TOO_SHORT", "FINDING_UNPUBLISHED"]);
22971
+ var SkippedAttackChainSchema = external_exports.object({
22972
+ attackChainId: external_exports.uuid(),
22973
+ title: external_exports.string(),
22974
+ reason: SkippedAttackChainReason
22975
+ });
22976
+ var PublishAllAttackChainsResponseSchema = external_exports.object({
22977
+ published: external_exports.array(PublishedAttackChainSchema),
22978
+ skipped: external_exports.array(SkippedAttackChainSchema)
22979
+ });
22879
22980
  var AttackChainStepFindingSchema = external_exports.object({
22880
22981
  pentestFindingId: external_exports.uuid(),
22881
22982
  displayId: external_exports.number().int().nullable(),
@@ -22884,12 +22985,15 @@ var AttackChainStepFindingSchema = external_exports.object({
22884
22985
  status: PentestFindingStatus,
22885
22986
  publishedAt: external_exports.coerce.date().nullable()
22886
22987
  });
22988
+ var AttackChainStatus = external_exports.enum(ATTACK_CHAIN_STATUS_VALUES);
22989
+ var AttackChainStepState = external_exports.enum(ATTACK_CHAIN_STEP_STATE_VALUES);
22887
22990
  var AttackChainStepSchema = external_exports.object({
22888
22991
  stepId: external_exports.uuid(),
22889
22992
  stepOrder: external_exports.number().int(),
22890
22993
  title: external_exports.string(),
22891
22994
  narrative: external_exports.string(),
22892
- findings: external_exports.array(AttackChainStepFindingSchema)
22995
+ findings: external_exports.array(AttackChainStepFindingSchema),
22996
+ remediationState: AttackChainStepState
22893
22997
  });
22894
22998
  var PentestAttackChainSchema = external_exports.object({
22895
22999
  attackChainId: external_exports.uuid(),
@@ -22900,6 +23004,7 @@ var PentestAttackChainSchema = external_exports.object({
22900
23004
  severity: PentestFindingSeverity,
22901
23005
  cvssVector: external_exports.string().nullable(),
22902
23006
  cvssScore: external_exports.number().nullable(),
23007
+ status: AttackChainStatus,
22903
23008
  publishedAt: external_exports.coerce.date().nullable(),
22904
23009
  createdAt: external_exports.coerce.date(),
22905
23010
  updatedAt: external_exports.coerce.date(),
@@ -23078,10 +23183,22 @@ var RepositoryResponseSchema = external_exports.object({
23078
23183
  primaryLanguage: external_exports.string().nullable().optional(),
23079
23184
  /** Staff-set opt-in: automated PR scans run on this repo even without a FULL SAST baseline. */
23080
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(),
23081
23188
  createdAt: external_exports.date(),
23082
23189
  updatedAt: external_exports.date()
23083
23190
  });
23084
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
+ });
23085
23202
  var SetPrScansEnabledRequestSchema = external_exports.object({
23086
23203
  enabled: external_exports.boolean()
23087
23204
  });
@@ -23449,6 +23566,14 @@ var FindingRewriteJsonSchema = external_exports.object({
23449
23566
  * falls back to live `f.status` in either case.
23450
23567
  */
23451
23568
  statusAtRewrite: PentestFindingStatus.nullish(),
23569
+ /**
23570
+ * Plain-language lead, two or three sentences, rendered before Description.
23571
+ * The cap is a paragraph, not a section: a summary that needs more room is a
23572
+ * description. `.default(null)` keeps rewrites persisted before the field
23573
+ * shipped parseable (the JSON has no key) while the output type stays
23574
+ * `string | null`, so every reader handles "no summary" the same way.
23575
+ */
23576
+ summary: external_exports.string().min(1).max(1e3).nullable().default(null),
23452
23577
  description: external_exports.string().min(1).max(12e3),
23453
23578
  rootCauseAnalysis: external_exports.string().min(1).max(12e3).nullable(),
23454
23579
  impact: external_exports.string().min(1).max(8e3),
@@ -23480,6 +23605,7 @@ var FindingRewritesJsonSchema = external_exports.object({
23480
23605
  path: ["rewrites"]
23481
23606
  });
23482
23607
  var FindingRewriteCorrectionFieldValues = [
23608
+ "summary",
23483
23609
  "description",
23484
23610
  "rootCauseAnalysis",
23485
23611
  "impact",
@@ -23557,6 +23683,7 @@ var ReportPdfResponseSchema = external_exports.object({
23557
23683
  var GetReportPdfResponseSchema = external_exports.object({
23558
23684
  report: ReportPdfResponseSchema.nullable()
23559
23685
  });
23686
+ var CvssVersionSchema = external_exports.enum(["3.1", "4.0"]);
23560
23687
  var ReportCanvasFindingSchema = external_exports.object({
23561
23688
  pentestFindingId: external_exports.uuid(),
23562
23689
  displayId: external_exports.int().min(1),
@@ -23710,6 +23837,12 @@ var ReportCanvasResponseSchema = external_exports.object({
23710
23837
  * project the default (true) — the value the first render will use. */
23711
23838
  includeRemediationTimeline: external_exports.boolean(),
23712
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(),
23713
23846
  // Counts reuse the module-local Severity/Status counts + totalFindings trio.
23714
23847
  liveSeverityCounts: SeverityCountsSchema,
23715
23848
  liveStatusCounts: StatusCountsSchema,
@@ -23908,6 +24041,11 @@ var IssuedReportSnapshotSchema = external_exports.object({
23908
24041
  * parses those rows for the changelog diff — a required field would throw
23909
24042
  * there and break issuing a new version of any pre-feature report. */
23910
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(),
23911
24049
  /** Types of the applications bound at issuance: the archival record of which
23912
24050
  * OWASP families the delivered PDF named. Optional for the same reason as
23913
24051
  * `testType` above: a pre-feature snapshot has no value, and
@@ -24198,6 +24336,9 @@ var PentestFindingRetestIdParamsSchema = PentestFindingIdParamsSchema.extend({
24198
24336
  var PentestFindingEvidenceIdParamsSchema = PentestFindingIdParamsSchema.extend({
24199
24337
  artifactId: external_exports.uuid()
24200
24338
  });
24339
+ var PentestFindingTimelineEntryParamsSchema = PentestFindingIdParamsSchema.extend({
24340
+ entryId: external_exports.uuid()
24341
+ });
24201
24342
  var CustomerExploitArtifactResponseSchema = external_exports.object({
24202
24343
  artifactId: external_exports.uuid(),
24203
24344
  kind: PentestExploitArtifactKind,
@@ -24275,6 +24416,8 @@ var PentestFindingResponseSchema = external_exports.object({
24275
24416
  */
24276
24417
  displayId: external_exports.number().int().positive().nullable(),
24277
24418
  title: external_exports.string(),
24419
+ /** Plain-language lead rendered before `description`; null on findings authored before it existed. */
24420
+ summary: external_exports.string().nullable(),
24278
24421
  description: external_exports.string(),
24279
24422
  severity: PentestFindingSeverity,
24280
24423
  category: external_exports.string(),
@@ -24310,6 +24453,7 @@ var PentestFindingResponseSchema = external_exports.object({
24310
24453
  externalTicket: ExternalTicketSummarySchema.nullable().default(null)
24311
24454
  });
24312
24455
  var PentestFindingListItemSchema = PentestFindingResponseSchema.omit({
24456
+ summary: true,
24313
24457
  description: true,
24314
24458
  proofOfConcept: true,
24315
24459
  remediation: true,
@@ -24432,14 +24576,29 @@ var ResolutionTimelineVia = external_exports.object({
24432
24576
  identifier: external_exports.string(),
24433
24577
  url: external_exports.url()
24434
24578
  }).nullable().default(null);
24579
+ var ResolutionTimelineEntrySource = external_exports.enum(["NOTE", "CONTEST_RESOLUTION", "RETEST"]);
24580
+ var ResolutionTimelineEditHandleSchema = external_exports.object({
24581
+ source: ResolutionTimelineEntrySource,
24582
+ id: external_exports.uuid()
24583
+ }).nullable().default(null);
24435
24584
  var ResolutionTimelineEntrySchema = external_exports.discriminatedUnion("kind", [
24585
+ // The finding became customer-visible. Derived from `publishedAt`, not from
24586
+ // an audit row, so every published finding carries exactly one regardless of
24587
+ // which path (batch publish, attack chain, legacy backfill) stamped it.
24588
+ external_exports.object({
24589
+ kind: external_exports.literal("published"),
24590
+ at: external_exports.coerce.date(),
24591
+ actor: external_exports.literal("TRACE").default("TRACE"),
24592
+ actorName: ResolutionTimelineActorName
24593
+ }),
24436
24594
  external_exports.object({
24437
24595
  kind: external_exports.literal("remediation_reported"),
24438
24596
  at: external_exports.coerce.date(),
24439
24597
  actor: ResolutionTimelineActor,
24440
24598
  actorName: ResolutionTimelineActorName,
24441
24599
  note: external_exports.string().nullable(),
24442
- via: ResolutionTimelineVia
24600
+ via: ResolutionTimelineVia,
24601
+ editHandle: ResolutionTimelineEditHandleSchema
24443
24602
  }),
24444
24603
  // Retests are run by Trace (staff or a Trace agent), so the actor is fixed;
24445
24604
  // the default keeps a web build ahead of the api parsing older payloads.
@@ -24449,14 +24608,16 @@ var ResolutionTimelineEntrySchema = external_exports.discriminatedUnion("kind",
24449
24608
  actor: external_exports.literal("TRACE").default("TRACE"),
24450
24609
  actorName: ResolutionTimelineActorName,
24451
24610
  outcome: RetestOutcome,
24452
- note: external_exports.string().nullable()
24611
+ note: external_exports.string().nullable(),
24612
+ editHandle: ResolutionTimelineEditHandleSchema
24453
24613
  }),
24454
24614
  external_exports.object({
24455
24615
  kind: external_exports.literal("accepted_with_controls"),
24456
24616
  at: external_exports.coerce.date(),
24457
24617
  actor: ResolutionTimelineActor,
24458
24618
  actorName: ResolutionTimelineActorName,
24459
- note: external_exports.string()
24619
+ note: external_exports.string(),
24620
+ editHandle: ResolutionTimelineEditHandleSchema
24460
24621
  }),
24461
24622
  external_exports.object({
24462
24623
  kind: external_exports.literal("dismissed"),
@@ -24464,7 +24625,8 @@ var ResolutionTimelineEntrySchema = external_exports.discriminatedUnion("kind",
24464
24625
  actor: ResolutionTimelineActor,
24465
24626
  actorName: ResolutionTimelineActorName,
24466
24627
  reason: PentestFindingDismissReason,
24467
- note: external_exports.string()
24628
+ note: external_exports.string(),
24629
+ editHandle: ResolutionTimelineEditHandleSchema
24468
24630
  }),
24469
24631
  // `note` nullable: webhook-driven reopens carry no customer note (the via
24470
24632
  // badge is the payload); human reopens always have one.
@@ -24475,7 +24637,8 @@ var ResolutionTimelineEntrySchema = external_exports.discriminatedUnion("kind",
24475
24637
  actorName: ResolutionTimelineActorName,
24476
24638
  toStatus: external_exports.enum(["OPEN", "IN_PROGRESS"]),
24477
24639
  note: external_exports.string().nullable(),
24478
- via: ResolutionTimelineVia
24640
+ via: ResolutionTimelineVia,
24641
+ editHandle: ResolutionTimelineEditHandleSchema
24479
24642
  }),
24480
24643
  // Legacy/unattributed note whose transition isn't on the WORM audit log.
24481
24644
  external_exports.object({
@@ -24483,7 +24646,8 @@ var ResolutionTimelineEntrySchema = external_exports.discriminatedUnion("kind",
24483
24646
  at: external_exports.coerce.date(),
24484
24647
  actor: ResolutionTimelineActor,
24485
24648
  actorName: ResolutionTimelineActorName,
24486
- note: external_exports.string()
24649
+ note: external_exports.string(),
24650
+ editHandle: ResolutionTimelineEditHandleSchema
24487
24651
  }),
24488
24652
  // Customer opened a dispute (PR 3). `note` = the customer's justification.
24489
24653
  external_exports.object({
@@ -24503,7 +24667,8 @@ var ResolutionTimelineEntrySchema = external_exports.discriminatedUnion("kind",
24503
24667
  actor: ResolutionTimelineActor,
24504
24668
  actorName: ResolutionTimelineActorName,
24505
24669
  resultStatus: PentestFindingStatus.nullable(),
24506
- note: external_exports.string()
24670
+ note: external_exports.string(),
24671
+ editHandle: ResolutionTimelineEditHandleSchema
24507
24672
  }),
24508
24673
  // An external ticket was created from this finding (PR 2). No `note` — the
24509
24674
  // identifier + url are the payload; actor is the customer who pushed it.
@@ -24650,7 +24815,24 @@ var RecoveredFindingResultSchema = external_exports.object({
24650
24815
  var RecoverStrandedFindingsResponseSchema = external_exports.object({
24651
24816
  results: external_exports.array(RecoveredFindingResultSchema)
24652
24817
  });
24818
+ var DraftMissingFindingSummariesRequestSchema = external_exports.object({
24819
+ /** Limit the run to these findings; omit for every finding in the pentest missing a summary. */
24820
+ pentestFindingIds: external_exports.array(external_exports.uuid()).max(500).optional()
24821
+ });
24822
+ var DraftedFindingSummaryResultSchema = external_exports.object({
24823
+ pentestFindingId: external_exports.uuid(),
24824
+ displayId: external_exports.int().positive().nullable(),
24825
+ status: external_exports.enum(["written", "failed"]),
24826
+ /** Failure shape only (no prose): what the drafter returned and what was expected. */
24827
+ reason: external_exports.string().optional()
24828
+ });
24829
+ var DraftMissingFindingSummariesResponseSchema = external_exports.object({
24830
+ /** Findings that already had a summary and were not touched. */
24831
+ skipped: external_exports.int().min(0),
24832
+ results: external_exports.array(DraftedFindingSummaryResultSchema)
24833
+ });
24653
24834
  var FindingProseSchema = FindingRewriteJsonSchema.pick({
24835
+ summary: true,
24654
24836
  description: true,
24655
24837
  rootCauseAnalysis: true,
24656
24838
  impact: true,
@@ -24660,8 +24842,11 @@ var FindingProseSchema = FindingRewriteJsonSchema.pick({
24660
24842
  remediation: true,
24661
24843
  verification: true
24662
24844
  });
24845
+ var FindingProsePatchSchema = FindingProseSchema.partial().extend({
24846
+ summary: FindingRewriteJsonSchema.shape.summary.unwrap().optional()
24847
+ });
24663
24848
  var ApplyFindingProseRequestSchema = external_exports.object({
24664
- prose: FindingProseSchema,
24849
+ prose: FindingProsePatchSchema,
24665
24850
  /**
24666
24851
  * Optimistic precondition — the finding's `updatedAt` at edit-open. A mismatch
24667
24852
  * means a concurrent writer (staff / report agent / retest) moved the row →
@@ -24683,7 +24868,9 @@ var CreatePentestFindingRequestSchema = external_exports.object({
24683
24868
  category: external_exports.string().min(1).max(200),
24684
24869
  status: PentestFindingStatus.optional(),
24685
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. */
24686
24872
  cvssVector: external_exports.string().max(200).optional(),
24873
+ /** Accepted for compatibility and ignored: the score is always derived from `cvssVector`. */
24687
24874
  cvssScore: external_exports.number().min(0).max(10).optional(),
24688
24875
  owaspCategory: external_exports.string().max(200).optional(),
24689
24876
  proofOfConcept: external_exports.string().max(5e4).optional(),
@@ -24707,7 +24894,9 @@ var UpdatePentestFindingRequestSchema = external_exports.object({
24707
24894
  category: external_exports.string().min(1).max(200).optional(),
24708
24895
  status: PentestFindingStatus.optional(),
24709
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. */
24710
24898
  cvssVector: external_exports.string().max(200).nullable().optional(),
24899
+ /** Accepted for compatibility and ignored: the score is always derived from `cvssVector`. */
24711
24900
  cvssScore: external_exports.number().min(0).max(10).nullable().optional(),
24712
24901
  owaspCategory: external_exports.string().max(200).nullable().optional(),
24713
24902
  proofOfConcept: external_exports.string().max(5e4).nullable().optional(),
@@ -24756,6 +24945,11 @@ var ResolvePentestFindingContestRequestSchema = external_exports.object({
24756
24945
  dismissReason: PentestFindingDismissReason.optional(),
24757
24946
  note: external_exports.string().min(1).max(1e4)
24758
24947
  });
24948
+ var UpdateResolutionTimelineEntryRequestSchema = external_exports.object({
24949
+ source: ResolutionTimelineEntrySource,
24950
+ note: external_exports.string().trim().min(1).max(1e4)
24951
+ });
24952
+ var UpdateResolutionTimelineEntryResponseSchema = external_exports.array(ResolutionTimelineEntrySchema);
24759
24953
  var PENTEST_FINDING_FEEDBACK_COMMENT_MAX_LENGTH = 5e3;
24760
24954
  var CreatePentestFindingFeedbackRequestSchema = external_exports.object({
24761
24955
  rating: PentestFindingFeedbackRating.nullable().default(null),
@@ -25110,6 +25304,12 @@ var PentestResponseSchema = external_exports.object({
25110
25304
  perHypothesisBudgetCapUsd: external_exports.number(),
25111
25305
  startedAt: external_exports.coerce.date().nullable(),
25112
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(),
25113
25313
  createdById: external_exports.uuid(),
25114
25314
  createdByName: external_exports.string(),
25115
25315
  createdAt: external_exports.coerce.date(),
@@ -25531,8 +25731,40 @@ var SshPrivateKeySecretSchema = external_exports.object({
25531
25731
  privateKeyPem: SshPrivateKeyPemSchema,
25532
25732
  passphrase: external_exports.string().min(1).max(SSH_PASSPHRASE_MAX_CHARS).optional()
25533
25733
  }).strict();
25734
+ var PasskeySecretSchema = external_exports.object({
25735
+ /** ECDSA P-256 private key, PKCS#8, base64. */
25736
+ privateKey: external_exports.string().min(1).max(4096),
25737
+ /** Opaque RP-assigned user id, base64 (≤64 bytes decoded). */
25738
+ userHandle: external_exports.string().max(128),
25739
+ userName: external_exports.string().max(320).optional(),
25740
+ userDisplayName: external_exports.string().max(320).optional(),
25741
+ /** largeBlob extension payload, base64. */
25742
+ largeBlob: external_exports.string().max(8192).optional()
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();
25534
25765
  var CredentialMfaKindValues = ["TOTP"];
25535
25766
  var CredentialMfaKind = external_exports.enum(CredentialMfaKindValues);
25767
+ var MFA_CAPABLE_CREDENTIAL_TYPES = ["USERNAME_PASSWORD", "MAGIC_AUTH"];
25536
25768
  var TotpSetupKeySchema = external_exports.string().min(1).max(TOTP_MAX_SECRET_CHARS * 4).superRefine((value, ctx) => {
25537
25769
  const message = totpSetupKeyError(value);
25538
25770
  if (message !== null) ctx.addIssue({ code: "custom", message });
@@ -25999,6 +26231,9 @@ var ValidationResultProseSchema = external_exports.object({
25999
26231
  // Optional/nullable on the wire for rolling-deploy safety (a pre-title host omits
26000
26232
  // it); the api falls back to the candidate hypothesis title. The host tool requires it.
26001
26233
  title: external_exports.string().min(1).max(300).nullable().optional(),
26234
+ // Same rolling-deploy shape as `title`: the host tool requires it, the wire
26235
+ // accepts its absence so a pre-summary host image still lands its verdict.
26236
+ summary: external_exports.string().min(1).max(1e3).nullable().optional(),
26002
26237
  description: external_exports.string().min(1).max(12e3),
26003
26238
  impact: external_exports.string().min(1).max(8e3),
26004
26239
  remediation: external_exports.string().min(1).max(12e3),
@@ -26080,12 +26315,14 @@ var RecordValidationResultRequestSchema = external_exports.object({
26080
26315
  */
26081
26316
  owaspCategory: external_exports.string().min(1).max(120).nullable().optional(),
26082
26317
  /**
26083
- * Agent-assigned CVSS v3.1 base vector for the demonstrated impact
26084
- * (PROVEN-only). The api derives and persists the canonical vector + base
26085
- * score AND the finding severity (from the score's band) server-side; an
26086
- * unparseable explicit vector is rejected (400). The score is NEVER
26087
- * client-supplied. Absent → both cvss columns null and severity falls back
26088
- * 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).
26089
26326
  */
26090
26327
  cvssVector: external_exports.string().min(1).max(120).nullable().optional(),
26091
26328
  /**
@@ -26345,7 +26582,22 @@ var AgentSessionCreateTestEmailRequestSchema = external_exports.object({
26345
26582
  * `too_big` blob for a purpose it then rephrased to something shorter and
26346
26583
  * equivalent. Validate what the target requires, not what our storage prefers.
26347
26584
  */
26348
- purpose: external_exports.string().min(1).max(200)
26585
+ purpose: external_exports.string().min(1).max(200),
26586
+ /**
26587
+ * Which Trace-managed mail domain to mint on. Omitted means the server's
26588
+ * default (`pentest.securewithtrace.com` unless overridden per environment).
26589
+ *
26590
+ * An enum rather than free text: the address is registered on the customer's
26591
+ * target as a login identity, so the set of domains mail can be minted on is
26592
+ * a Trace decision, not an agent one. The alternates exist for a target that
26593
+ * blocks or rate-limits one domain, or a check that needs two accounts whose
26594
+ * email domains differ.
26595
+ *
26596
+ * Reuse of an unsaved address is per domain: asking for the alternate never
26597
+ * hands back an address minted on the default, since the agent asked for a
26598
+ * different domain precisely because that one did not do.
26599
+ */
26600
+ domain: ManagedInboxDomainSchema.optional()
26349
26601
  });
26350
26602
  var AgentSessionCreateTestEmailResponseSchema = external_exports.object({
26351
26603
  /**
@@ -26528,6 +26780,74 @@ var AgentSessionSessionStateResponseSchema = external_exports.object({
26528
26780
  /** The decrypted, parsed captured browser session. */
26529
26781
  state: StorageStateSchema
26530
26782
  });
26783
+ var AgentSessionGetSshPrivateKeyRequestSchema = external_exports.object({
26784
+ runVersion: external_exports.number().int().nonnegative(),
26785
+ /** Which SSH_PRIVATE_KEY credential to fetch (from the non-secret env reference). */
26786
+ credentialId: external_exports.uuid()
26787
+ });
26788
+ var AgentSessionSshPrivateKeyResponseSchema = external_exports.object({
26789
+ credentialId: external_exports.uuid(),
26790
+ /** `Credential.updatedAt` (ISO): logged so a run names the revision it loaded. */
26791
+ revision: external_exports.iso.datetime(),
26792
+ /**
26793
+ * The decrypted key material. Returned VERBATIM as stored: the host seeds its
26794
+ * redactor with these exact bytes, and `redactString` matches exact substrings,
26795
+ * so normalizing line endings here would silently defeat redaction downstream.
26796
+ */
26797
+ secret: SshPrivateKeySecretSchema
26798
+ });
26799
+ var AgentSessionPasskeySchema = external_exports.object({
26800
+ /** Relying party id (a hostname). */
26801
+ rpId: external_exports.string().min(1).max(253),
26802
+ /** WebAuthn credential id, base64 (≤1023 bytes decoded). */
26803
+ webauthnCredentialId: external_exports.string().min(1).max(1400),
26804
+ /** ECDSA P-256 private key, PKCS#8, base64. */
26805
+ privateKey: external_exports.string().min(1).max(4096).optional(),
26806
+ /** Opaque RP-assigned user id, base64. */
26807
+ userHandle: external_exports.string().max(128).optional(),
26808
+ userName: external_exports.string().max(320).optional(),
26809
+ userDisplayName: external_exports.string().max(320).optional(),
26810
+ largeBlob: external_exports.string().max(8192).optional(),
26811
+ /** Signature counter as the authenticator reports it. Never -1 on this wire. */
26812
+ signCount: external_exports.number().int().min(0),
26813
+ backupEligibility: external_exports.boolean(),
26814
+ backupState: external_exports.boolean()
26815
+ });
26816
+ var AgentSessionListPasskeysRequestSchema = external_exports.object({
26817
+ runVersion: external_exports.number().int().nonnegative(),
26818
+ /** Which account (by `Credential.label`). Required: the host persists under it. */
26819
+ credentialLabel: external_exports.string().min(1).max(200)
26820
+ });
26821
+ var AgentSessionStoredPasskeySchema = AgentSessionPasskeySchema.extend({
26822
+ credentialPasskeyId: external_exports.uuid(),
26823
+ privateKey: external_exports.string().min(1).max(4096),
26824
+ userHandle: external_exports.string().max(128)
26825
+ });
26826
+ var AgentSessionListPasskeysResponseSchema = external_exports.object({
26827
+ credentialId: external_exports.uuid(),
26828
+ credentialLabel: external_exports.string().max(200),
26829
+ /** The account types a browser logs in as; the same set that can carry a second factor. */
26830
+ credentialType: external_exports.enum(MFA_CAPABLE_CREDENTIAL_TYPES),
26831
+ passkeys: external_exports.array(AgentSessionStoredPasskeySchema),
26832
+ /**
26833
+ * Stored rows whose envelope could not be read. Reported rather than swallowed so a
26834
+ * partial list is never presented to the agent as the whole store, and rather than
26835
+ * failing the call so the readable passkeys still load.
26836
+ */
26837
+ unreadable: external_exports.number().int().min(0)
26838
+ });
26839
+ var AgentSessionUpsertPasskeyRequestSchema = external_exports.object({
26840
+ runVersion: external_exports.number().int().nonnegative(),
26841
+ /** From the list response, never agent-supplied. */
26842
+ credentialId: external_exports.uuid(),
26843
+ passkey: AgentSessionPasskeySchema
26844
+ });
26845
+ var AgentSessionUpsertPasskeyResponseSchema = external_exports.object({
26846
+ credentialPasskeyId: external_exports.uuid(),
26847
+ created: external_exports.boolean(),
26848
+ /** The stored counter after the write (never lower than before). */
26849
+ signCount: external_exports.number().int().min(0)
26850
+ });
26531
26851
  var ParkAgentSessionRequestSchema = external_exports.object({
26532
26852
  runVersion: external_exports.number().int().nonnegative(),
26533
26853
  outcome: external_exports.enum(["IDLE", "WAITING_ON_STAFF", "FAILED"]),
@@ -27239,9 +27559,8 @@ var agentSessionInternalContract = c4.router({
27239
27559
  // Session type receives no account credentials, or its pinned toolset does
27240
27560
  // not carry these tools.
27241
27561
  403: ErrorResponseSchema,
27242
- // RUN_REVOKED only. Provider-side label collisions are resolved by
27243
- // re-suffixing rather than surfaced, and quota is a 422 — a bare 409 here
27244
- // would read as run revocation and kill the session.
27562
+ // RUN_REVOKED or INBOX_ALREADY_EXISTS. The host treats only RUN_REVOKED
27563
+ // as fatal; a concurrent label claim is a recoverable conflict.
27245
27564
  409: ErrorResponseSchema,
27246
27565
  // The org is at its managed-inbox quota (INBOX_QUOTA_EXCEEDED) — the agent
27247
27566
  // has no delete tool, so it must escalate rather than retry. Also
@@ -27344,8 +27663,78 @@ var agentSessionInternalContract = c4.router({
27344
27663
  401: ErrorResponseSchema,
27345
27664
  409: ErrorResponseSchema,
27346
27665
  // Credential not found, not SESSION_STATE, or outside the run's
27347
- // org/pentest/environments — no cross-tenant existence leak.
27666
+ // org/pentest/environments: no cross-tenant existence leak.
27667
+ 404: ErrorResponseSchema,
27668
+ 500: ErrorResponseSchema
27669
+ }
27670
+ },
27671
+ // get_ssh_private_key: the SESSION_STATE counterpart for SSH_PRIVATE_KEY. Same
27672
+ // scoping (org + pentest environments from the run token), same 404 for anything
27673
+ // outside it. The host calls this once per reference at boot and pipes the result
27674
+ // into ssh-add; nothing downstream can read the key back.
27675
+ getSshPrivateKey: {
27676
+ method: "POST",
27677
+ path: "/internal/v1/agent-sessions/:agentSessionId/ssh-private-key",
27678
+ pathParams: AgentSessionInternalParamsSchema,
27679
+ body: AgentSessionGetSshPrivateKeyRequestSchema,
27680
+ responses: {
27681
+ 200: AgentSessionSshPrivateKeyResponseSchema,
27682
+ 400: ErrorResponseSchema,
27683
+ 401: ErrorResponseSchema,
27684
+ 409: ErrorResponseSchema,
27685
+ // Credential not found, not SSH_PRIVATE_KEY, or outside the run's
27686
+ // org/pentest/environments: no cross-tenant existence leak.
27687
+ 404: ErrorResponseSchema,
27688
+ 500: ErrorResponseSchema
27689
+ }
27690
+ },
27691
+ // provision_passkey, read half: every passkey Trace holds for one of the run's
27692
+ // accounts, decrypted, so the host can load them into the virtual authenticator
27693
+ // it arms on the agent's browser. Same resolution and ownership gate as
27694
+ // enroll-mfa (org + pentest from the run token; label must resolve to exactly
27695
+ // one launch-visible credential). Returns 200 with an empty array for an
27696
+ // account with no passkeys yet: that is how the host learns the credentialId
27697
+ // it will persist a new one under.
27698
+ listPasskeys: {
27699
+ method: "POST",
27700
+ path: "/internal/v1/agent-sessions/:agentSessionId/passkeys",
27701
+ pathParams: AgentSessionInternalParamsSchema,
27702
+ body: AgentSessionListPasskeysRequestSchema,
27703
+ responses: {
27704
+ 200: AgentSessionListPasskeysResponseSchema,
27705
+ // Label on more than one environment (terminal), or a Trace fixture label.
27706
+ 400: ErrorResponseSchema,
27707
+ 401: ErrorResponseSchema,
27708
+ // Session type receives no account credentials.
27709
+ 403: ErrorResponseSchema,
27710
+ // No launch-visible credential with that label; same message for another
27711
+ // tenant's.
27348
27712
  404: ErrorResponseSchema,
27713
+ // RUN_REVOKED, including the liveness recheck inside the service.
27714
+ 409: ErrorResponseSchema,
27715
+ // The credential's type is not an account a browser logs in as.
27716
+ 422: ErrorResponseSchema,
27717
+ 500: ErrorResponseSchema
27718
+ }
27719
+ },
27720
+ // provision_passkey, write half: the host observed a passkey on the armed
27721
+ // authenticator (new, or its counter moved) and persists it. Keyed by the
27722
+ // credentialId the list call returned, never by label, and idempotent on
27723
+ // (credentialId, rpId, webauthnCredentialId) because the host retries on 5xx.
27724
+ upsertPasskey: {
27725
+ method: "POST",
27726
+ path: "/internal/v1/agent-sessions/:agentSessionId/passkeys/upsert",
27727
+ pathParams: AgentSessionInternalParamsSchema,
27728
+ body: AgentSessionUpsertPasskeyRequestSchema,
27729
+ responses: {
27730
+ 200: AgentSessionUpsertPasskeyResponseSchema,
27731
+ // PASSKEY_SECRET_REQUIRED: no row for the natural key and no key in the body.
27732
+ 400: ErrorResponseSchema,
27733
+ 401: ErrorResponseSchema,
27734
+ 403: ErrorResponseSchema,
27735
+ // credentialId not on this run's pentest under this org.
27736
+ 404: ErrorResponseSchema,
27737
+ 409: ErrorResponseSchema,
27349
27738
  500: ErrorResponseSchema
27350
27739
  }
27351
27740
  },
@@ -27515,6 +27904,7 @@ var agentSessionInternalContract = c4.router({
27515
27904
  body: OperatorApplyFindingCvssRequestSchema,
27516
27905
  responses: {
27517
27906
  200: OperatorFindingReadSchema,
27907
+ // INVALID_CVSS_VECTOR or CVSS_VERSION_MISMATCH (see the staff applyFindingCvss route).
27518
27908
  400: ErrorResponseSchema,
27519
27909
  401: ErrorResponseSchema,
27520
27910
  404: ErrorResponseSchema,
@@ -29541,11 +29931,12 @@ var CodeSizeLanguageTallySchema = external_exports.object({
29541
29931
  files: external_exports.number().int().nonnegative(),
29542
29932
  code: external_exports.number().int().nonnegative()
29543
29933
  });
29544
- var CodeSizePlatformValues = ["GITHUB", "BITBUCKET"];
29934
+ var CodeSizePlatformValues = ["GITHUB", "BITBUCKET", "UPLOAD"];
29545
29935
  var CodeSizeRepositorySchema = external_exports.object({
29546
29936
  repositoryId: external_exports.uuid(),
29547
29937
  repositoryUrl: external_exports.url(),
29548
29938
  platform: external_exports.enum(CodeSizePlatformValues),
29939
+ displayName: external_exports.string(),
29549
29940
  /** Billable SLOC of the current resolved branch. null = not measured yet. */
29550
29941
  billableSloc: external_exports.number().int().nonnegative().nullable()
29551
29942
  });
@@ -29557,7 +29948,8 @@ var CodeSizeBucketsSchema = external_exports.object({
29557
29948
  vendored_generated: CodeSizeBucketTallySchema
29558
29949
  });
29559
29950
  var OrganizationCodeSizeResponseSchema = external_exports.object({
29560
- /** 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
29561
29953
  * billableSloc desc (ties repositoryUrl asc), then unmeasured by
29562
29954
  * repositoryUrl asc — clients render as-is (spec-pr-2 §3.3). */
29563
29955
  repositories: external_exports.array(CodeSizeRepositorySchema),
@@ -29578,7 +29970,9 @@ var StaffCodeSizeRepositorySchema = external_exports.object({
29578
29970
  repositoryId: external_exports.uuid(),
29579
29971
  repositoryUrl: external_exports.url(),
29580
29972
  platform: external_exports.enum(CodeSizePlatformValues),
29581
- /** 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). */
29582
29976
  branch: external_exports.string(),
29583
29977
  /** null = no successful measurement for the current resolved branch. */
29584
29978
  billableSloc: external_exports.number().int().nonnegative().nullable(),
@@ -29596,7 +29990,8 @@ var StaffOrganizationCodeSizeResponseSchema = external_exports.object({
29596
29990
  totals: external_exports.object({
29597
29991
  billableSloc: external_exports.number().int().nonnegative(),
29598
29992
  rawLines: external_exports.number().int().nonnegative(),
29599
- /** 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. */
29600
29995
  repositoryCount: external_exports.number().int().nonnegative(),
29601
29996
  /** Repos with a measurement for their current resolved branch. */
29602
29997
  measuredCount: external_exports.number().int().nonnegative(),
@@ -29887,6 +30282,9 @@ var CredentialIdParamsSchema = OrgIdParamsSchema.extend({
29887
30282
  environmentId: external_exports.uuid(),
29888
30283
  credentialId: external_exports.uuid()
29889
30284
  });
30285
+ var CredentialPasskeyIdParamsSchema = CredentialIdParamsSchema.extend({
30286
+ credentialPasskeyId: external_exports.uuid()
30287
+ });
29890
30288
  var EnvironmentCredentialsParamsSchema = OrgIdParamsSchema.extend({
29891
30289
  environmentId: external_exports.uuid()
29892
30290
  });
@@ -30045,7 +30443,12 @@ var CredentialResponseSchema = external_exports.object({
30045
30443
  * parameters only — the seed lives in its own KMS envelope on the row and
30046
30444
  * has no reveal endpoint at all (see the reveal union below).
30047
30445
  */
30048
- 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)
30049
30452
  }).strict();
30050
30453
  var ListCredentialsResponseSchema = external_exports.array(CredentialResponseSchema);
30051
30454
  var UpdateCredentialRequestSchema = external_exports.object({
@@ -30268,6 +30671,27 @@ var credentialContract = c13.router({
30268
30671
  500: ErrorResponseSchema
30269
30672
  }
30270
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
+ },
30271
30695
  deleteCredential: {
30272
30696
  method: "DELETE",
30273
30697
  path: "/organizations/:organizationId/environments/:environmentId/credentials/:credentialId",
@@ -30704,7 +31128,13 @@ var VulnerabilityTypeValues = [
30704
31128
  "MISCONFIGURATION"
30705
31129
  ];
30706
31130
  var VulnerabilityType = external_exports.enum(VulnerabilityTypeValues);
30707
- var DismissReasonValues = ["ACCEPTED_RISK", "FALSE_POSITIVE", "OTHER"];
31131
+ var DismissReasonValues = [
31132
+ "ACCEPTED_RISK",
31133
+ "FALSE_POSITIVE",
31134
+ "OTHER",
31135
+ "INTENDED_BEHAVIOR",
31136
+ "DEFERRED"
31137
+ ];
30708
31138
  var DismissReason = external_exports.enum(DismissReasonValues);
30709
31139
 
30710
31140
  // ../../packages/api-contracts/src/schemas/api/v1/engagement.api.ts
@@ -30752,6 +31182,11 @@ var EngagementCardsSchema = external_exports.object({
30752
31182
  statusChangesViaPrComment: external_exports.number().int(),
30753
31183
  botCommentsPosted: external_exports.number().int(),
30754
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
+ */
30755
31190
  heuristicReplies: external_exports.number().int(),
30756
31191
  /** structuralReplies / max(1, botCommentsPosted), clamped to [0, 1]. */
30757
31192
  replyRate: external_exports.number().min(0).max(1)
@@ -31274,72 +31709,6 @@ var staffGithubContract = c18.router({
31274
31709
  }
31275
31710
  });
31276
31711
 
31277
- // ../../packages/api-contracts/src/schemas/api/v1/inbox.api.ts
31278
- var InboxLabelSchema = external_exports.string().regex(
31279
- /^[a-z][a-z0-9-]{0,39}$/,
31280
- "Inbox label must be lowercase letters/digits/dashes, starting with a letter, at most 40 characters"
31281
- );
31282
- var MAX_INBOX_LABEL_LENGTH = 40;
31283
- var MAX_LOCAL_PART_LENGTH = 64;
31284
- var MAX_INBOX_ADDRESS_PREFIX_LENGTH = MAX_LOCAL_PART_LENGTH - MAX_INBOX_LABEL_LENGTH - 1;
31285
- var ManagedInboxResponseSchema = external_exports.object({
31286
- /** Full derived address, e.g. `acme-qa-admin@pentest.securewithtrace.com`. */
31287
- address: external_exports.email(),
31288
- label: external_exports.string(),
31289
- createdAt: external_exports.date()
31290
- });
31291
- var ListManagedInboxesResponseSchema = external_exports.object({
31292
- inboxes: external_exports.array(ManagedInboxResponseSchema),
31293
- quota: external_exports.object({
31294
- used: external_exports.number().int().nonnegative(),
31295
- limit: external_exports.number().int().positive()
31296
- })
31297
- });
31298
- var CreateManagedInboxRequestSchema = external_exports.object({
31299
- label: InboxLabelSchema
31300
- }).strict();
31301
- var ManagedInboxLabelParamsSchema = OrgIdParamsSchema.extend({
31302
- label: external_exports.string().min(1).max(64)
31303
- });
31304
- var ManagedInboxMessageParamsSchema = ManagedInboxLabelParamsSchema.extend({
31305
- messageId: external_exports.string().min(1).max(500)
31306
- });
31307
- var ListInboxMessagesQuerySchema = external_exports.object({
31308
- /** Max messages, newest first. The provider may clamp further. */
31309
- limit: external_exports.coerce.number().int().positive().max(50).optional(),
31310
- /** Only messages received strictly after this instant. */
31311
- since: external_exports.coerce.date().optional()
31312
- }).strict();
31313
- var ManagedInboxMessageSummarySchema = external_exports.object({
31314
- messageId: external_exports.string().min(1).max(500),
31315
- from: external_exports.string().max(500),
31316
- /** Recipients as the provider returned them (display-name form allowed). */
31317
- to: external_exports.array(external_exports.string().max(500)),
31318
- subject: external_exports.string().max(1e3).nullable(),
31319
- preview: external_exports.string().max(2e3).nullable(),
31320
- receivedAt: external_exports.coerce.date(),
31321
- unread: external_exports.boolean()
31322
- });
31323
- var ListInboxMessagesResponseSchema = external_exports.object({
31324
- address: external_exports.email(),
31325
- label: external_exports.string(),
31326
- messages: external_exports.array(ManagedInboxMessageSummarySchema)
31327
- });
31328
- var GetInboxMessageResponseSchema = external_exports.object({
31329
- address: external_exports.email(),
31330
- label: external_exports.string(),
31331
- message: ManagedInboxMessageSummarySchema.extend({
31332
- body: external_exports.string(),
31333
- truncated: external_exports.boolean(),
31334
- /**
31335
- * http(s) links extracted from the message (hrefs first, then inline
31336
- * text URLs). Button-style mail often carries the magic link only in
31337
- * `href`, which a tag-stripped body can still miss if extraction fails.
31338
- */
31339
- links: external_exports.array(external_exports.string())
31340
- })
31341
- });
31342
-
31343
31712
  // ../../packages/api-contracts/src/contracts/v1/inbox.ts
31344
31713
  var c19 = initContract();
31345
31714
  var inboxContract = c19.router({
@@ -32517,9 +32886,35 @@ var PrScanAnalyticsMonthSchema = external_exports.object({
32517
32886
  /** Same metrics as the month, split by PR author. Sorted by findings, then PRs. */
32518
32887
  developers: external_exports.array(PrScanAnalyticsDeveloperSchema)
32519
32888
  });
32889
+ var PrScanAnalyticsClassCountSchema = external_exports.object({
32890
+ label: external_exports.string(),
32891
+ count: external_exports.number().int().nonnegative()
32892
+ });
32893
+ var PrScanAnalyticsTimeToFixSchema = external_exports.object({
32894
+ severity: VulnerabilitySeverity,
32895
+ fixedCount: external_exports.number().int().nonnegative(),
32896
+ /** Median minutes from the finding's first detection to the fixing scan; null when nothing was fixed. */
32897
+ medianMinutes: external_exports.number().nonnegative().nullable(),
32898
+ fixedWithinDayCount: external_exports.number().int().nonnegative()
32899
+ });
32900
+ var PrScanAnalyticsSummarySchema = external_exports.object({
32901
+ /** First completed PR scan on a tracked PR in the window; null when none. */
32902
+ since: external_exports.iso.datetime().nullable(),
32903
+ prCount: external_exports.number().int().nonnegative(),
32904
+ findingsCount: external_exports.number().int().nonnegative(),
32905
+ criticalHighCount: external_exports.number().int().nonnegative(),
32906
+ criticalHighFixedBeforeMergeCount: external_exports.number().int().nonnegative(),
32907
+ /** Critical/high fixed on a PR that is still open. */
32908
+ criticalHighFixedAwaitingMergeCount: external_exports.number().int().nonnegative(),
32909
+ /** Critical/high by weakness class, largest first, at most five rows with "Other" last. */
32910
+ criticalHighByClass: external_exports.array(PrScanAnalyticsClassCountSchema),
32911
+ /** One row per severity, CRITICAL first. */
32912
+ timeToFix: external_exports.array(PrScanAnalyticsTimeToFixSchema).length(VulnerabilitySeverityValues.length)
32913
+ });
32520
32914
  var PrScanAnalyticsResponseSchema = external_exports.object({
32521
32915
  generatedAt: external_exports.iso.datetime(),
32522
- months: external_exports.array(PrScanAnalyticsMonthSchema).length(12)
32916
+ months: external_exports.array(PrScanAnalyticsMonthSchema).length(12),
32917
+ summary: PrScanAnalyticsSummarySchema
32523
32918
  });
32524
32919
  var ProductPlanParamsSchema = external_exports.object({
32525
32920
  organizationId: external_exports.uuid(),
@@ -33793,6 +34188,26 @@ var pentestAttackChainContract = c29.router({
33793
34188
  500: ErrorResponseSchema
33794
34189
  }
33795
34190
  },
34191
+ /**
34192
+ * The "Publish all" control. Mirrors `pentestFindingContract.publishAllFindings`
34193
+ * (`/findings/publications`): a collection resource of publications rather than a verb
34194
+ * on the chain list.
34195
+ */
34196
+ publishAllAttackChains: {
34197
+ method: "POST",
34198
+ path: "/organizations/:organizationId/pentests/:pentestId/attack-chains/publications",
34199
+ pathParams: PentestAttackChainParamsSchema,
34200
+ body: PublishAllAttackChainsRequestSchema,
34201
+ responses: {
34202
+ 200: PublishAllAttackChainsResponseSchema,
34203
+ 400: ErrorResponseSchema,
34204
+ 401: ErrorResponseSchema,
34205
+ 403: ErrorResponseSchema,
34206
+ 404: ErrorResponseSchema,
34207
+ 409: ErrorResponseSchema,
34208
+ 500: ErrorResponseSchema
34209
+ }
34210
+ },
33796
34211
  unpublishAttackChain: {
33797
34212
  method: "POST",
33798
34213
  path: "/organizations/:organizationId/pentests/:pentestId/attack-chains/:attackChainId/unpublish",
@@ -34449,6 +34864,7 @@ var pentestFindingContract = c36.router({
34449
34864
  body: CreatePentestFindingRequestSchema,
34450
34865
  responses: {
34451
34866
  201: PentestFindingResponseSchema,
34867
+ // INVALID_CVSS_VECTOR / CVSS_VERSION_MISMATCH when a cvssVector is sent, or schema validation.
34452
34868
  400: ErrorResponseSchema,
34453
34869
  401: ErrorResponseSchema,
34454
34870
  403: ErrorResponseSchema,
@@ -34624,6 +35040,7 @@ var pentestFindingContract = c36.router({
34624
35040
  body: UpdatePentestFindingRequestSchema,
34625
35041
  responses: {
34626
35042
  200: PentestFindingResponseSchema,
35043
+ // INVALID_CVSS_VECTOR / CVSS_VERSION_MISMATCH when a cvssVector is sent, or schema validation.
34627
35044
  400: ErrorResponseSchema,
34628
35045
  401: ErrorResponseSchema,
34629
35046
  403: ErrorResponseSchema,
@@ -34647,6 +35064,22 @@ var pentestFindingContract = c36.router({
34647
35064
  500: ErrorResponseSchema
34648
35065
  }
34649
35066
  },
35067
+ draftMissingFindingSummaries: {
35068
+ method: "POST",
35069
+ path: "/organizations/:organizationId/pentests/:pentestId/findings/summaries/draft",
35070
+ pathParams: PentestFindingParamsSchema,
35071
+ body: DraftMissingFindingSummariesRequestSchema,
35072
+ responses: {
35073
+ 200: DraftMissingFindingSummariesResponseSchema,
35074
+ 400: ErrorResponseSchema,
35075
+ 401: ErrorResponseSchema,
35076
+ 403: ErrorResponseSchema,
35077
+ 404: ErrorResponseSchema,
35078
+ // PENTEST_CLOSED (reopen first).
35079
+ 409: ErrorResponseSchema,
35080
+ 500: ErrorResponseSchema
35081
+ }
35082
+ },
34650
35083
  applyFindingCvss: {
34651
35084
  method: "PUT",
34652
35085
  path: "/organizations/:organizationId/pentests/:pentestId/findings/:pentestFindingId/cvss",
@@ -34654,7 +35087,8 @@ var pentestFindingContract = c36.router({
34654
35087
  body: ApplyFindingCvssRequestSchema,
34655
35088
  responses: {
34656
35089
  200: PentestFindingResponseSchema,
34657
- // 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.
34658
35092
  400: ErrorResponseSchema,
34659
35093
  401: ErrorResponseSchema,
34660
35094
  403: ErrorResponseSchema,
@@ -34924,6 +35358,26 @@ var pentestFindingContract = c36.router({
34924
35358
  },
34925
35359
  // Note CRUD removed in PR 2 — the resolution timeline replaces the read
34926
35360
  // surface; resolution workflows + the operator agent write notes directly.
35361
+ // ─── Resolution timeline ───
35362
+ // Staff-only: rewrite the customer-visible note behind one history entry.
35363
+ // `entryId` + `body.source` come from the entry's `editHandle` on the staff GET.
35364
+ updateTimelineEntry: {
35365
+ method: "PATCH",
35366
+ path: "/organizations/:organizationId/pentests/:pentestId/findings/:pentestFindingId/timeline-entries/:entryId",
35367
+ pathParams: PentestFindingTimelineEntryParamsSchema,
35368
+ body: UpdateResolutionTimelineEntryRequestSchema,
35369
+ responses: {
35370
+ 200: UpdateResolutionTimelineEntryResponseSchema,
35371
+ 400: ErrorResponseSchema,
35372
+ 401: ErrorResponseSchema,
35373
+ // Non-staff caller, or the entry is customer-authored.
35374
+ 403: ErrorResponseSchema,
35375
+ 404: ErrorResponseSchema,
35376
+ // PENTEST_CLOSED.
35377
+ 409: ErrorResponseSchema,
35378
+ 500: ErrorResponseSchema
35379
+ }
35380
+ },
34927
35381
  // ─── AI parsing ───
34928
35382
  parseFindingMarkdown: {
34929
35383
  method: "POST",
@@ -35852,6 +36306,25 @@ var repositoryContract = c42.router({
35852
36306
  500: ErrorResponseSchema
35853
36307
  }
35854
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
+ },
35855
36328
  getScanningBranchPreference: {
35856
36329
  method: "GET",
35857
36330
  path: "/organizations/:organizationId/repositories/scanning-branch/preference",
@@ -36175,11 +36648,17 @@ var StaffOrganizationPentestSummarySchema = external_exports.object({
36175
36648
  findingCount: external_exports.int().nonnegative(),
36176
36649
  startedAt: external_exports.iso.datetime().nullable(),
36177
36650
  endedAt: external_exports.iso.datetime().nullable(),
36178
- 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()
36179
36654
  }).strict();
36180
36655
  var ListStaffOrganizationPentestsResponseSchema = external_exports.array(
36181
36656
  StaffOrganizationPentestSummarySchema
36182
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();
36183
36662
 
36184
36663
  // ../../packages/api-contracts/src/contracts/v1/staff-pentests.ts
36185
36664
  var c45 = initContract();
@@ -36209,6 +36688,21 @@ var staffPentestsContract = c45.router({
36209
36688
  500: ErrorResponseSchema
36210
36689
  },
36211
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)"
36212
36706
  }
36213
36707
  });
36214
36708
 
@@ -36733,7 +37227,10 @@ var VulnerabilityLatestFindingSchema = external_exports.object({
36733
37227
  classification: external_exports.enum(["vulnerability", "defense_in_depth", "context_dependent"]).nullable(),
36734
37228
  prerequisiteAccess: external_exports.string().nullable(),
36735
37229
  capabilityGain: external_exports.string().nullable(),
36736
- 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()
36737
37234
  });
36738
37235
  var DependencyGraphSchema = external_exports.object({
36739
37236
  vulnerablePackage: external_exports.string(),
@@ -37105,6 +37602,7 @@ var vulnerabilityContract = c50.router({
37105
37602
  402: ErrorResponseSchema,
37106
37603
  403: ErrorResponseSchema,
37107
37604
  404: ErrorResponseSchema,
37605
+ 409: ErrorResponseSchema,
37108
37606
  500: ErrorResponseSchema
37109
37607
  }
37110
37608
  },
@@ -37888,7 +38386,7 @@ async function requestDeviceAuthorization(clientId) {
37888
38386
  }
37889
38387
  return await response.json();
37890
38388
  }
37891
- var sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
38389
+ var sleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
37892
38390
  async function pollForToken(clientId, deviceCode, options = {}) {
37893
38391
  const expiresIn = options.expiresIn ?? 300;
37894
38392
  let interval = options.interval ?? 5;
@@ -38573,18 +39071,18 @@ var proto = Object.defineProperties(() => {
38573
39071
  }
38574
39072
  }
38575
39073
  });
38576
- var createStyler = (open3, close, parent) => {
39074
+ var createStyler = (open4, close, parent) => {
38577
39075
  let openAll;
38578
39076
  let closeAll;
38579
39077
  if (parent === void 0) {
38580
- openAll = open3;
39078
+ openAll = open4;
38581
39079
  closeAll = close;
38582
39080
  } else {
38583
- openAll = parent.openAll + open3;
39081
+ openAll = parent.openAll + open4;
38584
39082
  closeAll = close + parent.closeAll;
38585
39083
  }
38586
39084
  return {
38587
- open: open3,
39085
+ open: open4,
38588
39086
  close,
38589
39087
  openAll,
38590
39088
  closeAll,
@@ -39167,6 +39665,9 @@ function formatStatus(status) {
39167
39665
  function tsvSafe(value) {
39168
39666
  return (value ?? "").replace(/[\t\r\n]/g, " ");
39169
39667
  }
39668
+ function formulaSafe(value) {
39669
+ return /^[=+\-@]/.test(value) ? `'${value}` : value;
39670
+ }
39170
39671
 
39171
39672
  // src/pager.ts
39172
39673
  function isInteractiveEnvironment() {
@@ -39281,6 +39782,21 @@ function extractRepoName(input) {
39281
39782
  }
39282
39783
  return input;
39283
39784
  }
39785
+ function normalizeRepoUrlForMatch(url2) {
39786
+ let normalized = url2.trim().toLowerCase();
39787
+ const scp = normalized.match(/^[a-z0-9._-]+@([^:/]+):(.+)$/);
39788
+ if (scp) {
39789
+ normalized = `${scp[1]}/${scp[2]}`;
39790
+ } else {
39791
+ normalized = normalized.replace(/^[a-z][a-z0-9+.-]*:\/\//, "").replace(/^[a-z0-9._-]+@/, "");
39792
+ }
39793
+ let previous;
39794
+ do {
39795
+ previous = normalized;
39796
+ normalized = normalized.replace(/\/+$/, "").replace(/\.git$/, "");
39797
+ } while (normalized !== previous);
39798
+ return normalized;
39799
+ }
39284
39800
  var BRANCH_NAME_PATTERN = /^[a-zA-Z0-9._/+-]+$/;
39285
39801
  var TRACE_GIT_ASKPASS_TOKEN_ENV = "TRACE_GIT_ASKPASS_TOKEN";
39286
39802
  var DEFAULT_LIMIT = 30;
@@ -39288,6 +39804,8 @@ var DEFAULT_PAGE = 1;
39288
39804
  var MAX_LIMIT = 100;
39289
39805
  var REPOSITORY_PLATFORMS = ["GITHUB", "GITLAB", "BITBUCKET"];
39290
39806
  var REPOSITORY_STATUSES = ["CONNECTED", "DISCONNECTED"];
39807
+ var SCANNING_INCLUSIONS = ["included", "excluded"];
39808
+ var SCANNING_INCLUSION_BATCH_SIZE = 500;
39291
39809
  function validateBranchName(branch) {
39292
39810
  if (branch.startsWith("-")) {
39293
39811
  throw new Error(`Invalid branch name: '${branch}' \u2014 branch names cannot start with a dash`);
@@ -39341,6 +39859,18 @@ function normalizeStatus(value, source) {
39341
39859
  `${source} must be one of: ${REPOSITORY_STATUSES.join(", ")}. Received '${value}'.`
39342
39860
  );
39343
39861
  }
39862
+ function normalizeScanningInclusion(value, source) {
39863
+ const normalized = value.trim().toLowerCase();
39864
+ if (SCANNING_INCLUSIONS.includes(normalized)) {
39865
+ return normalized;
39866
+ }
39867
+ throw new Error(
39868
+ `${source} must be one of: ${SCANNING_INCLUSIONS.join(", ")}. Received '${value}'.`
39869
+ );
39870
+ }
39871
+ function scanningInclusionOf(repo) {
39872
+ return repo.includedInScanning ? "included" : "excluded";
39873
+ }
39344
39874
  function normalizeFilterField(rawField) {
39345
39875
  const field = rawField.trim().toLowerCase();
39346
39876
  switch (field) {
@@ -39365,9 +39895,14 @@ function normalizeFilterField(rawField) {
39365
39895
  case "primarylanguage":
39366
39896
  case "primary_language":
39367
39897
  return "language";
39898
+ case "scanning":
39899
+ case "inclusion":
39900
+ case "included_in_scanning":
39901
+ case "includedinscanning":
39902
+ return "scanning";
39368
39903
  default:
39369
39904
  throw new Error(
39370
- `Unknown repository filter field '${rawField}'. Supported fields: platform, status, url, repo, branch, language.`
39905
+ `Unknown repository filter field '${rawField}'. Supported fields: platform, status, url, repo, branch, language, scanning.`
39371
39906
  );
39372
39907
  }
39373
39908
  }
@@ -39411,6 +39946,9 @@ function parseRepositoryFilterExpression(expression) {
39411
39946
  if (field === "status") {
39412
39947
  normalizeStatus(value, "--filter");
39413
39948
  }
39949
+ if (field === "scanning") {
39950
+ normalizeScanningInclusion(value, "--filter");
39951
+ }
39414
39952
  return { field, operator, value };
39415
39953
  }
39416
39954
  function doesValueMatchFilter(candidate, filterValue, operator, exactMatchOnly) {
@@ -39418,7 +39956,7 @@ function doesValueMatchFilter(candidate, filterValue, operator, exactMatchOnly)
39418
39956
  const normalizedFilter = normalizeForMatching(filterValue);
39419
39957
  if (exactMatchOnly || operator === "exact" || operator === "not_exact") {
39420
39958
  const matches2 = normalizedCandidate === normalizedFilter;
39421
- return operator === "not_exact" ? !matches2 : matches2;
39959
+ return operator === "not_exact" || operator === "not_contains" ? !matches2 : matches2;
39422
39960
  }
39423
39961
  const matches = normalizedCandidate.includes(normalizedFilter);
39424
39962
  return operator === "not_contains" ? !matches : matches;
@@ -39454,6 +39992,13 @@ function matchParsedFilter(repo, parsedFilter) {
39454
39992
  const language = repo.primaryLanguage ?? "";
39455
39993
  return doesValueMatchFilter(language, parsedFilter.value, parsedFilter.operator, false);
39456
39994
  }
39995
+ case "scanning":
39996
+ return doesValueMatchFilter(
39997
+ scanningInclusionOf(repo),
39998
+ parsedFilter.value,
39999
+ parsedFilter.operator,
40000
+ true
40001
+ );
39457
40002
  }
39458
40003
  }
39459
40004
  function filterRepositories(repositories, options) {
@@ -39465,6 +40010,7 @@ function filterRepositories(repositories, options) {
39465
40010
  const normalizedQuery = options.query ? normalizeForMatching(options.query) : void 0;
39466
40011
  const normalizedDefaultBranch = options.githubDefaultBranch ? normalizeForMatching(options.githubDefaultBranch) : void 0;
39467
40012
  const normalizedLanguage = options.language ? normalizeForMatching(options.language) : void 0;
40013
+ const normalizedScanning = options.scanning ? normalizeScanningInclusion(options.scanning, "--scanning") : void 0;
39468
40014
  return repositories.filter((repo) => {
39469
40015
  if (normalizedPlatform && repo.platform.toUpperCase() !== normalizedPlatform) {
39470
40016
  return false;
@@ -39472,6 +40018,9 @@ function filterRepositories(repositories, options) {
39472
40018
  if (normalizedStatus && repo.status.toUpperCase() !== normalizedStatus) {
39473
40019
  return false;
39474
40020
  }
40021
+ if (normalizedScanning && scanningInclusionOf(repo) !== normalizedScanning) {
40022
+ return false;
40023
+ }
39475
40024
  if (normalizedDefaultBranch && !normalizeForMatching(repo.githubDefaultBranch).includes(normalizedDefaultBranch)) {
39476
40025
  return false;
39477
40026
  }
@@ -39544,7 +40093,9 @@ function paginateResults(items, options) {
39544
40093
  };
39545
40094
  }
39546
40095
  function printRepositoriesTsv(repositories) {
39547
- console.log("repositoryUrl platform status githubDefaultBranch scanningBranch language");
40096
+ console.log(
40097
+ "repositoryUrl platform status githubDefaultBranch scanningBranch language scanning"
40098
+ );
39548
40099
  for (const repo of repositories) {
39549
40100
  console.log(
39550
40101
  [
@@ -39553,18 +40104,20 @@ function printRepositoriesTsv(repositories) {
39553
40104
  tsvSafe(repo.status),
39554
40105
  tsvSafe(repo.githubDefaultBranch),
39555
40106
  tsvSafe(repo.scanningBranch ?? ""),
39556
- tsvSafe(repo.primaryLanguage)
40107
+ tsvSafe(repo.primaryLanguage),
40108
+ scanningInclusionOf(repo)
39557
40109
  ].join(" ")
39558
40110
  );
39559
40111
  }
39560
40112
  }
39561
40113
  function buildRepositoriesTable(repositories) {
39562
- const headers = ["Repository URL", "Platform", "Status"];
40114
+ const headers = ["Repository URL", "Platform", "Status", "Scanning"];
39563
40115
  const rows = repositories.map(
39564
40116
  (repo) => [
39565
40117
  stripAnsi(String(repo.repositoryUrl)),
39566
40118
  stripAnsi(String(repo.platform)).toUpperCase(),
39567
- repo.status === "CONNECTED" ? "connected" : "disconnected"
40119
+ repo.status === "CONNECTED" ? "connected" : "disconnected",
40120
+ scanningInclusionOf(repo)
39568
40121
  ]
39569
40122
  );
39570
40123
  const colWidths = headers.map(
@@ -39693,13 +40246,27 @@ async function maybeRefreshCloneTokenSessionForOrganization(organizationId, list
39693
40246
  }
39694
40247
  return refreshForOrganization(workosOrganizationId);
39695
40248
  }
40249
+ function resolveScanningFlag(options) {
40250
+ if (options.included && options.excluded) {
40251
+ throw new Error("Pass only one of --included / --excluded.");
40252
+ }
40253
+ if (options.included) {
40254
+ return "included";
40255
+ }
40256
+ if (options.excluded) {
40257
+ return "excluded";
40258
+ }
40259
+ return void 0;
40260
+ }
39696
40261
  function toRepositoryListFilterOptions(options) {
40262
+ const scanning = resolveScanningFlag(options);
39697
40263
  return {
39698
40264
  ...options.platform ? { platform: options.platform } : {},
39699
40265
  ...options.status ? { status: options.status } : {},
39700
40266
  ...options.query ? { query: options.query } : {},
39701
40267
  ...options.defaultBranch ? { githubDefaultBranch: options.defaultBranch } : {},
39702
40268
  ...options.language ? { language: options.language } : {},
40269
+ ...scanning ? { scanning } : {},
39703
40270
  ...options.filter.length > 0 ? { filters: options.filter } : {}
39704
40271
  };
39705
40272
  }
@@ -39712,9 +40279,9 @@ function addRepositoryFilterOptions(command) {
39712
40279
  "--status <status>",
39713
40280
  "Filter by status (CONNECTED, DISCONNECTED)",
39714
40281
  (value) => value.toUpperCase()
39715
- ).option("--query <query>", "Case-insensitive search across URL, repo, branch, and language").option("--default-branch <branch>", "Filter by default branch (case-insensitive contains)").option("--language <language>", "Filter by primary language (case-insensitive contains)").option(
40282
+ ).option("--query <query>", "Case-insensitive search across URL, repo, branch, and language").option("--default-branch <branch>", "Filter by default branch (case-insensitive contains)").option("--language <language>", "Filter by primary language (case-insensitive contains)").option("--included", "Only repositories included in scanning").option("--excluded", "Only repositories excluded from scanning").option(
39716
40283
  "-f, --filter <expression>",
39717
- "Advanced filter expression. Repeatable. Examples: platform=GITHUB, repo:trace, status!=DISCONNECTED",
40284
+ "Advanced filter expression. Repeatable. Examples: platform=GITHUB, repo:trace, status!=DISCONNECTED, scanning=excluded",
39718
40285
  collectFilterExpression,
39719
40286
  []
39720
40287
  ).option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").option(
@@ -39742,6 +40309,14 @@ async function runRepositoryListCommand(options) {
39742
40309
  }
39743
40310
  return;
39744
40311
  }
40312
+ let filterOptions;
40313
+ try {
40314
+ filterOptions = toRepositoryListFilterOptions(options);
40315
+ } catch (error48) {
40316
+ const message = error48 instanceof Error ? error48.message : "Unknown filter parsing error.";
40317
+ console.error(`Invalid repository filter: ${message}`);
40318
+ process.exit(1);
40319
+ }
39745
40320
  const organizationId = await resolveOrganizationId();
39746
40321
  if (options.dependency) {
39747
40322
  await runDependencyFilter(organizationId, {
@@ -39765,14 +40340,14 @@ async function runRepositoryListCommand(options) {
39765
40340
  process.exit(1);
39766
40341
  }
39767
40342
  try {
39768
- filteredRepositories = filterRepositories(result.body, toRepositoryListFilterOptions(options));
40343
+ filteredRepositories = filterRepositories(result.body, filterOptions);
39769
40344
  } catch (error48) {
39770
40345
  const message = error48 instanceof Error ? error48.message : "Unknown filter parsing error.";
39771
40346
  console.error(`Invalid repository filter: ${message}`);
39772
40347
  process.exit(1);
39773
40348
  }
39774
40349
  if (filteredRepositories.length === 0) {
39775
- if (options.platform || options.status || options.query || options.defaultBranch || options.language || options.filter.length > 0) {
40350
+ if (options.platform || options.status || options.query || options.defaultBranch || options.language || options.included || options.excluded || options.filter.length > 0) {
39776
40351
  console.log("No repositories found matching the provided filters.");
39777
40352
  return;
39778
40353
  }
@@ -39906,6 +40481,8 @@ Examples:
39906
40481
  tracecli repo list --output json | jq '.[].repositoryUrl'
39907
40482
  tracecli repo list --output tsv | cut -f1
39908
40483
  tracecli repo list --filter "language:typescript" --limit 10
40484
+ tracecli repo list --excluded
40485
+ tracecli repo list --query payments --included --output json
39909
40486
  tracecli repo list --dependency lodash@4.17.21
39910
40487
  tracecli repo list --web`
39911
40488
  ).action(async (options) => {
@@ -39923,6 +40500,7 @@ Examples:
39923
40500
  tracecli repo filter --page 2 --limit 30
39924
40501
  tracecli repo filter --all
39925
40502
  tracecli repo filter --language typescript --limit 10
40503
+ tracecli repo filter --filter "scanning=excluded"
39926
40504
  tracecli repo filter --output json | jq '.[].repositoryUrl'
39927
40505
  tracecli repo filter --output tsv | cut -f1
39928
40506
  tracecli repo filter --web`
@@ -40285,12 +40863,158 @@ Examples:
40285
40863
  await runSetScanningBranchCommand(repositoryUrl, options);
40286
40864
  });
40287
40865
  }
40866
+ function resolveRepositoryReferences(repositories, references) {
40867
+ const byId = /* @__PURE__ */ new Map();
40868
+ const byUrl = /* @__PURE__ */ new Map();
40869
+ for (const repo of repositories) {
40870
+ byId.set(repo.repositoryId.toLowerCase(), repo);
40871
+ byUrl.set(normalizeRepoUrlForMatch(repo.repositoryUrl), repo);
40872
+ }
40873
+ const matched = /* @__PURE__ */ new Map();
40874
+ const unresolved = [];
40875
+ for (const reference of references) {
40876
+ const trimmed = reference.trim();
40877
+ if (trimmed.length === 0) {
40878
+ continue;
40879
+ }
40880
+ const repo = byId.get(trimmed.toLowerCase()) ?? byUrl.get(normalizeRepoUrlForMatch(resolveRepoUrl(trimmed)));
40881
+ if (!repo) {
40882
+ unresolved.push(trimmed);
40883
+ continue;
40884
+ }
40885
+ if (!matched.has(repo.repositoryId)) {
40886
+ matched.set(repo.repositoryId, repo);
40887
+ }
40888
+ }
40889
+ return { matched: [...matched.values()], unresolved };
40890
+ }
40891
+ function scanningInclusionVerb(includedInScanning) {
40892
+ return includedInScanning ? { past: "Included", noun: "included in scanning" } : { past: "Excluded", noun: "excluded from scanning" };
40893
+ }
40894
+ async function runSetScanningInclusionCommand(references, includedInScanning, options) {
40895
+ const organizationId = await resolveOrganizationId();
40896
+ const listResult = await authenticatedRequest(
40897
+ (client) => client.repository.listRepositories({ params: { organizationId } })
40898
+ );
40899
+ if (listResult.status === 401) {
40900
+ console.error("Authentication failed. Run `tracecli auth login` to re-authenticate.");
40901
+ process.exit(1);
40902
+ }
40903
+ if (listResult.status !== 200) {
40904
+ console.error(`Failed to list repositories (status ${listResult.status}).`);
40905
+ process.exit(1);
40906
+ }
40907
+ const { matched, unresolved } = resolveRepositoryReferences(listResult.body, references);
40908
+ if (unresolved.length > 0) {
40909
+ console.error(
40910
+ colorError(
40911
+ `${unresolved.length === 1 ? "Repository" : "Repositories"} not found in this organization: ${unresolved.join(", ")}`
40912
+ )
40913
+ );
40914
+ console.error(
40915
+ "Pass a repository ID, URL, or owner/repo shorthand. Run `tracecli repo list` to see connected repositories."
40916
+ );
40917
+ process.exit(1);
40918
+ }
40919
+ if (matched.length === 0) {
40920
+ console.error(colorError("No repositories given."));
40921
+ process.exit(1);
40922
+ }
40923
+ const outcomes = matched.map((repo) => ({
40924
+ repositoryId: repo.repositoryId,
40925
+ repositoryUrl: repo.repositoryUrl,
40926
+ previouslyIncludedInScanning: repo.includedInScanning
40927
+ }));
40928
+ const ids = matched.map((repo) => repo.repositoryId);
40929
+ let updatedCount = 0;
40930
+ const notFoundRepositoryIds = [];
40931
+ const verb = scanningInclusionVerb(includedInScanning);
40932
+ for (let offset = 0; offset < ids.length; offset += SCANNING_INCLUSION_BATCH_SIZE) {
40933
+ const result = await authenticatedRequest(
40934
+ (client) => client.repository.setScanningInclusion({
40935
+ params: { organizationId },
40936
+ body: {
40937
+ repositoryIds: ids.slice(offset, offset + SCANNING_INCLUSION_BATCH_SIZE),
40938
+ includedInScanning
40939
+ }
40940
+ })
40941
+ );
40942
+ if (result.status === 200) {
40943
+ updatedCount += result.body.updatedCount;
40944
+ notFoundRepositoryIds.push(...result.body.notFoundRepositoryIds);
40945
+ continue;
40946
+ }
40947
+ const committedSuffix = offset > 0 ? ` ${updatedCount} of ${ids.length} repositories were already updated.` : "";
40948
+ if (result.status === 401) {
40949
+ console.error("Authentication failed. Run `tracecli auth login` to re-authenticate.");
40950
+ } else if (result.status === 403) {
40951
+ console.error(
40952
+ colorError(
40953
+ `You don't have permission to change repository scanning in this organization (analyst or admin role required).${committedSuffix}`
40954
+ )
40955
+ );
40956
+ } else {
40957
+ const message = extractErrorMessage(result.body);
40958
+ console.error(
40959
+ colorError(
40960
+ `Failed to update repository scanning (status ${result.status})${message ? `: ${message}` : "."}${committedSuffix}`
40961
+ )
40962
+ );
40963
+ }
40964
+ process.exit(1);
40965
+ }
40966
+ if (options.output === "json") {
40967
+ console.log(
40968
+ JSON.stringify(
40969
+ { includedInScanning, updatedCount, notFoundRepositoryIds, repositories: outcomes },
40970
+ null,
40971
+ 2
40972
+ )
40973
+ );
40974
+ return;
40975
+ }
40976
+ const notFound = new Set(notFoundRepositoryIds);
40977
+ for (const outcome of outcomes) {
40978
+ if (notFound.has(outcome.repositoryId)) {
40979
+ console.log(
40980
+ `${colorWarningPrefix("Skipped")} ${outcome.repositoryUrl}: no longer connected to this organization`
40981
+ );
40982
+ } else if (outcome.previouslyIncludedInScanning === includedInScanning) {
40983
+ console.log(`Already ${verb.noun}: ${outcome.repositoryUrl}`);
40984
+ } else {
40985
+ console.log(`${colorSuccess(verb.past)} ${outcome.repositoryUrl}`);
40986
+ }
40987
+ }
40988
+ const changedCount = outcomes.filter(
40989
+ (outcome) => !notFound.has(outcome.repositoryId) && outcome.previouslyIncludedInScanning !== includedInScanning
40990
+ ).length;
40991
+ console.log(
40992
+ `${changedCount} of ${outcomes.length} ${outcomes.length === 1 ? "repository" : "repositories"} changed to ${verb.noun}.${includedInScanning ? "" : " Excluded repositories are skipped by every scan until you include them again."}`
40993
+ );
40994
+ }
40995
+ function createScanningInclusionCommand(includedInScanning) {
40996
+ const name = includedInScanning ? "include" : "exclude";
40997
+ const description = includedInScanning ? "Include repositories in scanning" : "Exclude repositories from scanning (no scans of any kind run until included again)";
40998
+ return new Command(name).description(description).argument("<repository...>", "Repository ID, URL, or owner/repo shorthand (repeatable)").option("-o, --output <format>", "Output format: table (default), json", "table").addHelpText(
40999
+ "after",
41000
+ `
41001
+ Examples:
41002
+ tracecli repo ${name} acme/api
41003
+ tracecli repo ${name} https://github.com/acme/api https://bitbucket.org/acme/worker
41004
+ tracecli repo ${name} 3f1c6c2e-9b7a-4a6f-8f1d-2d4e5a6b7c8d --output json
41005
+ tracecli repo list --query legacy --output json | jq -r '.[].repositoryUrl' | xargs tracecli repo ${name}`
41006
+ ).action(async (repositories, options) => {
41007
+ await runSetScanningInclusionCommand(repositories, includedInScanning, options);
41008
+ });
41009
+ }
40288
41010
  function createRepoCommand() {
40289
41011
  const repo = new Command("repo").description("Manage repositories");
40290
41012
  repo.addCommand(createListCommand());
40291
41013
  repo.addCommand(createFilterCommand());
40292
41014
  repo.addCommand(createCloneCommand());
40293
41015
  repo.addCommand(createSetScanningBranchCommand());
41016
+ repo.addCommand(createScanningInclusionCommand(true));
41017
+ repo.addCommand(createScanningInclusionCommand(false));
40294
41018
  return repo;
40295
41019
  }
40296
41020
 
@@ -40399,21 +41123,6 @@ function validateCreateTypeUrl(type, url2) {
40399
41123
  }
40400
41124
  return null;
40401
41125
  }
40402
- function normalizeRepoUrlForMatch(url2) {
40403
- let normalized = url2.trim().toLowerCase();
40404
- const scp = normalized.match(/^[a-z0-9._-]+@([^:/]+):(.+)$/);
40405
- if (scp) {
40406
- normalized = `${scp[1]}/${scp[2]}`;
40407
- } else {
40408
- normalized = normalized.replace(/^[a-z][a-z0-9+.-]*:\/\//, "").replace(/^[a-z0-9._-]+@/, "");
40409
- }
40410
- let previous;
40411
- do {
40412
- previous = normalized;
40413
- normalized = normalized.replace(/\/+$/, "").replace(/\.git$/, "");
40414
- } while (normalized !== previous);
40415
- return normalized;
40416
- }
40417
41126
  function buildCreateApplicationBody(input) {
40418
41127
  return {
40419
41128
  name: input.name,
@@ -43367,6 +44076,9 @@ function repoDisplayName(repositoryUrl) {
43367
44076
  return repositoryUrl;
43368
44077
  }
43369
44078
  }
44079
+ function repoLabel(repo) {
44080
+ return sanitizeDisplay(repo.displayName ?? repoDisplayName(repo.repositoryUrl));
44081
+ }
43370
44082
  function countedRows(totals) {
43371
44083
  const b = totals.buckets;
43372
44084
  return [
@@ -43423,7 +44135,7 @@ function renderCodeSizeTable(codeSize, options) {
43423
44135
  lines.push(
43424
44136
  ...padValues(
43425
44137
  shown.map((repo) => ({
43426
- label: repoDisplayName(repo.repositoryUrl),
44138
+ label: repoLabel(repo),
43427
44139
  value: repo.billableSloc ?? 0
43428
44140
  }))
43429
44141
  )
@@ -43438,7 +44150,8 @@ function renderCodeSizeTsv(codeSize) {
43438
44150
  return [
43439
44151
  "repository billableSloc",
43440
44152
  ...codeSize.repositories.map(
43441
- (repo) => `${repoDisplayName(repo.repositoryUrl)} ${repo.billableSloc === null ? "" : repo.billableSloc}`
44153
+ // Upload names are customer-authored, and TSV lands in spreadsheets.
44154
+ (repo) => `${formulaSafe(repoLabel(repo))} ${repo.billableSloc === null ? "" : repo.billableSloc}`
43442
44155
  )
43443
44156
  ];
43444
44157
  }
@@ -45425,7 +46138,7 @@ function collectAdditionalUrl(value, previous) {
45425
46138
  function buildCreateEnvironmentBody(input) {
45426
46139
  return {
45427
46140
  name: input.name,
45428
- primaryUrl: input.primaryUrl,
46141
+ ...input.primaryUrl !== void 0 ? { primaryUrl: input.primaryUrl } : {},
45429
46142
  ...input.additionalUrls !== void 0 && input.additionalUrls.length > 0 ? { additionalUrls: input.additionalUrls } : {},
45430
46143
  ...input.description !== void 0 ? { description: input.description } : {},
45431
46144
  ...input.isProduction !== void 0 ? { isProduction: input.isProduction } : {}
@@ -45909,13 +46622,20 @@ async function runDelete4(environmentId, options) {
45909
46622
  console.log(colorSuccess(`Deleted environment ${environmentId}`));
45910
46623
  }
45911
46624
  function createCreateCommand4() {
45912
- return new Command("create").description("Create an environment for an application").argument("<name>", "Environment name (lowercase letters/digits/dashes)").requiredOption("--app <applicationId>", "Parent application UUID").requiredOption("--url <url>", "Primary URL for the environment").option("--additional-url <url>", "Extra probe URL (repeatable)", collectAdditionalUrl).option("--description <text>", "Description").option("--production", "Mark as the production environment").option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").addHelpText(
46625
+ return new Command("create").description("Create an environment for an application").argument("<name>", "Environment name (lowercase letters/digits/dashes)").requiredOption("--app <applicationId>", "Parent application UUID").option(
46626
+ "--url <url>",
46627
+ "Primary URL for the environment (web and API applications only; omit for CLI and desktop)"
46628
+ ).option("--additional-url <url>", "Extra probe URL (repeatable)", collectAdditionalUrl).option("--description <text>", "Description").option("--production", "Mark as the production environment").option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").addHelpText(
45913
46629
  "after",
45914
46630
  `
46631
+ A web or API application requires --url. A CLI or desktop application is a
46632
+ credential context with no network target and must omit it.
46633
+
45915
46634
  Examples:
45916
46635
  tracecli environment create staging --app <appId> --url https://staging.acme.com
45917
46636
  tracecli environment create production --app <appId> --url https://acme.com --production
45918
- tracecli environment create production --app <appId> --url https://app.acme.com --additional-url https://api.acme.com`
46637
+ tracecli environment create production --app <appId> --url https://app.acme.com --additional-url https://api.acme.com
46638
+ tracecli environment create pentest-sandbox --app <cliAppId>`
45919
46639
  ).action(async (name, options) => {
45920
46640
  await runCreate4(name, options);
45921
46641
  });
@@ -46304,14 +47024,839 @@ async function runCreateFinding(options) {
46304
47024
  });
46305
47025
  }
46306
47026
  function createCreateFindingCommand() {
46307
- 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) => {
47027
+ return new Command("create").description("Create a pentest finding from a contractor write-up (staff)").requiredOption("--pentest <id>", "Pentest display ID (e.g. PT-3) or UUID").option("--title <text>", "Finding title").option("--description <text>", "Finding description").option("--description-file <path>", "Read description from a file").option("--severity <severity>", `Severity (${SEVERITY_LIST})`).option("--category <text>", "Finding category (e.g. Broken Access Control)").option("--cwe <id>", "CWE identifier (repeatable)", collectCwe).option(
47028
+ "--cvss-vector <vector>",
47029
+ "CVSS base vector (CVSS:3.1/... or CVSS:4.0/...); the score is derived server-side"
47030
+ ).option("--cvss-score <n>", "Ignored: the score is derived from --cvss-vector").option("--owasp <category>", "OWASP category").option("--poc <text>", "Proof of concept").option("--poc-file <path>", "Read proof of concept from a file").option("--remediation <text>", "Remediation guidance").option("--remediation-file <path>", "Read remediation from a file").option("--file <path>", "JSON object of finding fields; flags override").option("-o, --output <format>", "Output format: text, json (default: text)", "text").action(async (options) => {
46308
47031
  await runCreateFinding(options);
46309
47032
  });
46310
47033
  }
46311
47034
 
46312
- // src/pentest/evidence.ts
46313
- import { readFileSync as readFileSync6, statSync as statSync3 } from "fs";
47035
+ // src/pentest/deliverables.ts
47036
+ import { open as open2, rename, rm, stat } from "fs/promises";
47037
+ import { basename as basename3, join as join6, resolve } from "path";
47038
+
47039
+ // src/upload/prepare.ts
47040
+ import { createHash } from "crypto";
47041
+ import { createReadStream, statSync as statSync3 } from "fs";
47042
+ import { open } from "fs/promises";
46314
47043
  import { basename as basename2 } from "path";
47044
+ var CodeUploadPrepareError = class extends Error {
47045
+ constructor(message) {
47046
+ super(message);
47047
+ this.name = "CodeUploadPrepareError";
47048
+ }
47049
+ };
47050
+ var KIND_BY_INPUT = {
47051
+ source: "SOURCE_ARCHIVE",
47052
+ code: "SOURCE_ARCHIVE",
47053
+ "source-archive": "SOURCE_ARCHIVE",
47054
+ source_archive: "SOURCE_ARCHIVE",
47055
+ SOURCE_ARCHIVE: "SOURCE_ARCHIVE",
47056
+ apk: "ANDROID_APK",
47057
+ android: "ANDROID_APK",
47058
+ "android-apk": "ANDROID_APK",
47059
+ android_apk: "ANDROID_APK",
47060
+ ANDROID_APK: "ANDROID_APK",
47061
+ ipa: "IOS_IPA",
47062
+ ios: "IOS_IPA",
47063
+ "ios-ipa": "IOS_IPA",
47064
+ ios_ipa: "IOS_IPA",
47065
+ IOS_IPA: "IOS_IPA"
47066
+ };
47067
+ function normalizeArtifactKind(input) {
47068
+ const trimmed = input.trim();
47069
+ return KIND_BY_INPUT[trimmed] ?? KIND_BY_INPUT[trimmed.toLowerCase()] ?? null;
47070
+ }
47071
+ function inferArtifactKindFromFilename(filename) {
47072
+ const lower = filename.toLowerCase();
47073
+ if (lower.endsWith(".zip")) return "SOURCE_ARCHIVE";
47074
+ if (lower.endsWith(".apk")) return "ANDROID_APK";
47075
+ if (lower.endsWith(".ipa")) return "IOS_IPA";
47076
+ return null;
47077
+ }
47078
+ function requiredExtensionForKind(kind) {
47079
+ if (kind === "ANDROID_APK") return ".apk";
47080
+ if (kind === "IOS_IPA") return ".ipa";
47081
+ return ".zip";
47082
+ }
47083
+ function defaultDisplayName(originalFilename) {
47084
+ const base = originalFilename.replace(/\.(zip|apk|ipa)$/i, "").trim();
47085
+ return base.slice(0, 200) || "source";
47086
+ }
47087
+ function formatArchiveBytes(bytes) {
47088
+ if (bytes < 1024) return `${bytes} B`;
47089
+ if (bytes < 1024 * 1024) {
47090
+ const kb = bytes / 1024;
47091
+ return `${kb.toFixed(Number.isInteger(kb) ? 0 : 1)} KB`;
47092
+ }
47093
+ if (bytes < 1024 * 1024 * 1024) {
47094
+ const mb = bytes / (1024 * 1024);
47095
+ return `${mb.toFixed(Number.isInteger(mb) ? 0 : 1)} MB`;
47096
+ }
47097
+ const gb = bytes / (1024 * 1024 * 1024);
47098
+ return `${gb.toFixed(Number.isInteger(gb) ? 0 : 1)} GB`;
47099
+ }
47100
+ function archiveTooLargeMessage() {
47101
+ return `The file must be ${formatArchiveBytes(CODE_UPLOAD_MAX_BYTES)} or smaller.`;
47102
+ }
47103
+ async function hashFileSha256(filePath) {
47104
+ const hash2 = createHash("sha256");
47105
+ const stream = createReadStream(filePath);
47106
+ for await (const chunk of stream) {
47107
+ hash2.update(chunk);
47108
+ }
47109
+ return hash2.digest("hex");
47110
+ }
47111
+ async function readFileSlice(filePath, start, endExclusive) {
47112
+ const length = endExclusive - start;
47113
+ if (!Number.isSafeInteger(start) || !Number.isSafeInteger(length) || start < 0 || length < 1) {
47114
+ throw new Error(
47115
+ `Invalid file slice: start=${String(start)} endExclusive=${String(endExclusive)}.`
47116
+ );
47117
+ }
47118
+ const buffer = Buffer.alloc(length);
47119
+ const handle = await open(filePath, "r");
47120
+ try {
47121
+ const { bytesRead } = await handle.read(buffer, 0, length, start);
47122
+ if (bytesRead !== length) {
47123
+ throw new Error(
47124
+ `Expected to read ${String(length)} bytes at offset ${String(start)}, got ${String(bytesRead)}.`
47125
+ );
47126
+ }
47127
+ return buffer;
47128
+ } finally {
47129
+ await handle.close();
47130
+ }
47131
+ }
47132
+ function assertPreparedMatchesUpload(upload, prepared) {
47133
+ if (upload.sizeBytes !== prepared.sizeBytes || upload.archiveSha256 !== prepared.archiveSha256) {
47134
+ throw new CodeUploadPrepareError(
47135
+ "The file does not match the interrupted upload (size or SHA-256 differs)."
47136
+ );
47137
+ }
47138
+ }
47139
+ async function prepareUploadFile(filePath, options = {}) {
47140
+ let sizeBytes;
47141
+ try {
47142
+ const stats = statSync3(filePath);
47143
+ if (!stats.isFile()) {
47144
+ throw new CodeUploadPrepareError("Path is not a regular file. Pass a .zip, .apk, or .ipa.");
47145
+ }
47146
+ sizeBytes = stats.size;
47147
+ } catch (error48) {
47148
+ if (error48 instanceof CodeUploadPrepareError) throw error48;
47149
+ throw new CodeUploadPrepareError("Cannot read that file.");
47150
+ }
47151
+ if (!Number.isSafeInteger(sizeBytes) || sizeBytes < 1) {
47152
+ throw new CodeUploadPrepareError("The file is empty.");
47153
+ }
47154
+ if (sizeBytes > CODE_UPLOAD_MAX_BYTES) {
47155
+ throw new CodeUploadPrepareError(archiveTooLargeMessage());
47156
+ }
47157
+ const originalFilename = basename2(filePath);
47158
+ const inferred = inferArtifactKindFromFilename(originalFilename);
47159
+ const artifactKind = options.artifactKind ?? inferred;
47160
+ if (artifactKind == null) {
47161
+ throw new CodeUploadPrepareError("Pass a .zip source archive, an .apk, or an .ipa.");
47162
+ }
47163
+ const expected = requiredExtensionForKind(artifactKind);
47164
+ if (!originalFilename.toLowerCase().endsWith(expected)) {
47165
+ const label = isApplicationBuildArtifactKind(artifactKind) ? artifactKind === "ANDROID_APK" ? "Android APK" : "iOS package" : "source archive";
47166
+ throw new CodeUploadPrepareError(`A ${label} must end in ${expected}.`);
47167
+ }
47168
+ const displayName = (options.displayName ?? defaultDisplayName(originalFilename)).trim();
47169
+ if (displayName.length < 1 || displayName.length > 200) {
47170
+ throw new CodeUploadPrepareError("Display name must be 1 to 200 characters.");
47171
+ }
47172
+ return {
47173
+ path: filePath,
47174
+ displayName,
47175
+ originalFilename,
47176
+ sizeBytes,
47177
+ archiveSha256: await hashFileSha256(filePath),
47178
+ artifactKind
47179
+ };
47180
+ }
47181
+
47182
+ // src/pentest/status-update.ts
47183
+ var PENTEST_IDENTIFIER_PATTERN = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|pt-\d+|\d+)$/i;
47184
+ var FINDING_IDENTIFIER_PATTERN = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|ptf-\d+|\d+)$/i;
47185
+ var MAX_NOTE_LENGTH = 1e4;
47186
+ function parseStatusUpdateTarget(raw) {
47187
+ const normalized = raw.trim().toUpperCase().replace(/[\s-]+/g, "_");
47188
+ if (normalized === "IN_REVIEW" || normalized === "REVIEW") {
47189
+ return {
47190
+ error: "IN_REVIEW is not a finding status. Use IN_PROGRESS while fixing, then REMEDIATED when the fix is ready for retest."
47191
+ };
47192
+ }
47193
+ if (normalized === "IN_PROGRESS" || normalized === "REMEDIATED") {
47194
+ return normalized;
47195
+ }
47196
+ return {
47197
+ error: `Invalid --status '${sanitizeDisplay(raw)}'. Use IN_PROGRESS or REMEDIATED.`
47198
+ };
47199
+ }
47200
+ function isPentestIdentifier(value) {
47201
+ return PENTEST_IDENTIFIER_PATTERN.test(value);
47202
+ }
47203
+ function isFindingIdentifier(value) {
47204
+ return FINDING_IDENTIFIER_PATTERN.test(value);
47205
+ }
47206
+ function failStatusUpdate(status, body, viaApiKey) {
47207
+ const { message, errorCode } = readErrorFields(body);
47208
+ if (status === UNAUTHORIZED_STATUS2) {
47209
+ exitAuthFailed();
47210
+ }
47211
+ if (status === 403 && viaApiKey) {
47212
+ console.error(
47213
+ colorError(
47214
+ `${message ?? "Request failed (status 403)."}
47215
+ API keys can update finding status once Trace enables finding automation for your organization. Ask Trace to turn it on, or run this with \`tracecli auth login\`.`
47216
+ )
47217
+ );
47218
+ process.exit(1);
47219
+ }
47220
+ if (status === 409 && errorCode === "FINDING_STALE") {
47221
+ console.error(
47222
+ colorError(message ?? "The finding changed while this update was in flight. Retry it.")
47223
+ );
47224
+ process.exit(1);
47225
+ }
47226
+ if (status === 404) {
47227
+ console.error(colorError(message ?? "Pentest or finding not found."));
47228
+ process.exit(1);
47229
+ }
47230
+ if (status === 409) {
47231
+ if (errorCode === "PENTEST_CLOSED") {
47232
+ console.error(
47233
+ colorError(message ?? "Closed pentests must be reopened before updating findings.")
47234
+ );
47235
+ } else if (errorCode === "FINDING_TRANSITION_NOT_ALLOWED") {
47236
+ console.error(
47237
+ colorError(
47238
+ message ?? "That status change is not allowed from the finding's current status."
47239
+ )
47240
+ );
47241
+ } else {
47242
+ console.error(colorError(message ?? "Conflict (status 409)."));
47243
+ }
47244
+ process.exit(1);
47245
+ }
47246
+ if (status === 400 || status === 403) {
47247
+ console.error(colorError(message ?? `Request failed (status ${status}).`));
47248
+ process.exit(1);
47249
+ }
47250
+ console.error(
47251
+ colorError(
47252
+ `Failed to update finding status (status ${status})${message ? `: ${message}` : "."}`
47253
+ )
47254
+ );
47255
+ process.exit(1);
47256
+ }
47257
+ async function runStatusUpdate(options) {
47258
+ if (options.output !== "text" && options.output !== "json") {
47259
+ console.error(
47260
+ colorError(`Invalid --output '${sanitizeDisplay(options.output)}'. Use text or json.`)
47261
+ );
47262
+ process.exit(1);
47263
+ }
47264
+ const target = parseStatusUpdateTarget(options.status);
47265
+ if (typeof target !== "string") {
47266
+ console.error(colorError(target.error));
47267
+ process.exit(1);
47268
+ }
47269
+ if (!isPentestIdentifier(options.pentest)) {
47270
+ console.error(
47271
+ colorError(
47272
+ `Invalid pentest identifier '${sanitizeDisplay(options.pentest)}'. Use a display ID (e.g. PT-3) or UUID.`
47273
+ )
47274
+ );
47275
+ process.exit(1);
47276
+ }
47277
+ if (!isFindingIdentifier(options.finding)) {
47278
+ console.error(
47279
+ colorError(
47280
+ `Invalid finding identifier '${sanitizeDisplay(options.finding)}'. Use a finding number (e.g. PTF-7) or UUID.`
47281
+ )
47282
+ );
47283
+ process.exit(1);
47284
+ }
47285
+ const note = options.note?.trim();
47286
+ if (note !== void 0 && note.length === 0) {
47287
+ console.error(colorError("--note cannot be empty."));
47288
+ process.exit(1);
47289
+ }
47290
+ if (note !== void 0 && note.length > MAX_NOTE_LENGTH) {
47291
+ console.error(
47292
+ colorError(`--note is ${note.length} characters; the limit is ${MAX_NOTE_LENGTH}.`)
47293
+ );
47294
+ process.exit(1);
47295
+ }
47296
+ if (note !== void 0 && target === "IN_PROGRESS") {
47297
+ console.error(
47298
+ colorError("--note is only used when reporting remediation (--status REMEDIATED).")
47299
+ );
47300
+ process.exit(1);
47301
+ }
47302
+ const viaApiKey = requireAuth().kind === "api_key";
47303
+ const organizationId = await resolveOrganizationId();
47304
+ const params = {
47305
+ organizationId,
47306
+ pentestId: options.pentest,
47307
+ pentestFindingId: options.finding
47308
+ };
47309
+ const result = viaApiKey ? await authenticatedRequest(
47310
+ (client) => client.pentestFinding.automationStatusUpdate({
47311
+ params,
47312
+ body: { status: target, ...note !== void 0 ? { note } : {} }
47313
+ })
47314
+ ) : target === "IN_PROGRESS" ? await authenticatedRequest(
47315
+ (client) => client.pentestFinding.startProgress({ params, body: {} })
47316
+ ) : await authenticatedRequest(
47317
+ (client) => client.pentestFinding.reportRemediation({
47318
+ params,
47319
+ body: note !== void 0 ? { note } : {}
47320
+ })
47321
+ );
47322
+ if (result.status !== 200) {
47323
+ failStatusUpdate(result.status, result.body, viaApiKey);
47324
+ }
47325
+ if (options.output === "json") {
47326
+ console.log(JSON.stringify(result.body, null, 2));
47327
+ return;
47328
+ }
47329
+ const displayId = result.body.displayId;
47330
+ const status = sanitizeDisplay(result.body.status);
47331
+ console.log(colorSuccess(`PTF-${displayId} is now ${status}`));
47332
+ }
47333
+ function createStatusUpdateCommand() {
47334
+ return new Command("status-update").description("Move a pentest finding to IN_PROGRESS or REMEDIATED").requiredOption("--pentest <id>", "Pentest display ID (e.g. PT-3) or UUID").requiredOption("--finding <id>", "Finding number (e.g. PTF-7) or UUID").requiredOption(
47335
+ "--status <status>",
47336
+ "IN_PROGRESS (start work) or REMEDIATED (ready for retest)"
47337
+ ).option("--note <text>", "Optional context when reporting remediation").option("-o, --output <format>", "Output format: text, json (default: text)", "text").addHelpText(
47338
+ "after",
47339
+ `
47340
+ Customer-facing status writes \u2014 the same transitions as the dashboard. An agent
47341
+ can mark a finding in progress when it starts the fix, then remediated when the
47342
+ fix is ready for Trace to re-test. Leaving the finding IN_PROGRESS (and not
47343
+ sending REMEDIATED) keeps it out of the retest queue.
47344
+
47345
+ IN_PROGRESS is allowed from OPEN or VERIFIED_OPEN. REMEDIATED is allowed from
47346
+ OPEN, IN_PROGRESS, or VERIFIED_OPEN.
47347
+
47348
+ A signed-in session (\`tracecli auth login\`) attributes the change to you. A
47349
+ Trace API key works too, once Trace enables finding automation for your
47350
+ organization: those writes are recorded as \`api-key:<key name>\` and show up as
47351
+ yours in the finding's history.
47352
+
47353
+ Examples:
47354
+ tracecli pentest findings status-update --pentest PT-3 --finding 7 --status IN_PROGRESS
47355
+ tracecli pentest findings status-update --pentest PT-3 --finding PTF-7 --status REMEDIATED
47356
+ tracecli pentest findings status-update --pentest PT-3 --finding 7 --status REMEDIATED \\
47357
+ --note "Fixed in PR 412" --output json`
47358
+ ).action(async (options) => {
47359
+ await runStatusUpdate(options);
47360
+ });
47361
+ }
47362
+
47363
+ // src/pentest/deliverables.ts
47364
+ var PentestDeliverableDownloadError = class extends Error {
47365
+ constructor(message) {
47366
+ super(message);
47367
+ this.name = "PentestDeliverableDownloadError";
47368
+ }
47369
+ };
47370
+ function safeDeliverableFilename(documentId, extension, fallbackStem) {
47371
+ const stripped = Array.from(basename3(documentId.replace(/\\/g, "/")), (char) => {
47372
+ const code = char.codePointAt(0) ?? 0;
47373
+ return code <= 31 || code === 127 ? "_" : char;
47374
+ }).join("").trim();
47375
+ const stem = stripped.length === 0 || stripped === "." || stripped === ".." ? fallbackStem : stripped;
47376
+ return stem.toLowerCase().endsWith(extension) ? stem : `${stem}${extension}`;
47377
+ }
47378
+ function reportFileName(report, artifact) {
47379
+ if (artifact === "docx") {
47380
+ return safeDeliverableFilename(report.documentId, ".docx", "pentest-report");
47381
+ }
47382
+ const extension = report.downloadFormat === "bundle" ? ".zip" : ".pdf";
47383
+ return safeDeliverableFilename(report.documentId, extension, "pentest-report");
47384
+ }
47385
+ function attestationFileName(attestation) {
47386
+ return safeDeliverableFilename(attestation.documentId, ".pdf", "letter-of-attestation");
47387
+ }
47388
+ async function resolveDeliverableDestination(fileName, outOption) {
47389
+ if (outOption === void 0) return resolve(process.cwd(), fileName);
47390
+ const target = resolve(outOption);
47391
+ try {
47392
+ const info = await stat(target);
47393
+ if (info.isDirectory()) return join6(target, fileName);
47394
+ } catch {
47395
+ }
47396
+ return target;
47397
+ }
47398
+ async function pathExists(path) {
47399
+ try {
47400
+ await stat(path);
47401
+ return true;
47402
+ } catch {
47403
+ return false;
47404
+ }
47405
+ }
47406
+ async function downloadDeliverableToFile(downloadUrl, destination, options = {}) {
47407
+ const fetchImpl = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
47408
+ const partialPath = `${destination}.part`;
47409
+ let response;
47410
+ try {
47411
+ response = await fetchImpl(downloadUrl, { method: "GET", redirect: "manual" });
47412
+ } catch (error48) {
47413
+ const detail = error48 instanceof Error ? error48.message : "network error";
47414
+ throw new PentestDeliverableDownloadError(`Download from storage failed: ${detail}.`);
47415
+ }
47416
+ if (!response.ok || response.body === null) {
47417
+ if (response.body !== null) await response.body.cancel();
47418
+ throw new PentestDeliverableDownloadError(
47419
+ `Storage answered HTTP ${response.status} instead of the document. Retry; the download link may have expired.`
47420
+ );
47421
+ }
47422
+ const contentLength = response.headers.get("content-length");
47423
+ let totalBytes = null;
47424
+ if (contentLength !== null) {
47425
+ const declared = Number(contentLength);
47426
+ if (!Number.isSafeInteger(declared) || declared < 0) {
47427
+ await response.body.cancel();
47428
+ throw new PentestDeliverableDownloadError(
47429
+ `Storage declared an unreadable Content-Length ('${contentLength}').`
47430
+ );
47431
+ }
47432
+ totalBytes = declared;
47433
+ }
47434
+ await rm(partialPath, { force: true });
47435
+ const file2 = await open2(partialPath, "wx");
47436
+ const reader = response.body.getReader();
47437
+ let downloadedBytes = 0;
47438
+ options.onProgress?.({ downloadedBytes, totalBytes });
47439
+ try {
47440
+ for (; ; ) {
47441
+ const chunk = await reader.read();
47442
+ if (chunk.done) break;
47443
+ downloadedBytes += chunk.value.byteLength;
47444
+ if (totalBytes !== null && downloadedBytes > totalBytes) {
47445
+ await reader.cancel();
47446
+ throw new PentestDeliverableDownloadError(
47447
+ `Storage sent more than the ${totalBytes} bytes it declared.`
47448
+ );
47449
+ }
47450
+ await writeAll(file2, chunk.value);
47451
+ options.onProgress?.({ downloadedBytes, totalBytes });
47452
+ }
47453
+ await file2.close();
47454
+ if (totalBytes !== null && downloadedBytes !== totalBytes) {
47455
+ throw new PentestDeliverableDownloadError(
47456
+ `Download ended after ${downloadedBytes} of ${totalBytes} bytes.`
47457
+ );
47458
+ }
47459
+ if (downloadedBytes === 0) {
47460
+ throw new PentestDeliverableDownloadError("Storage returned an empty document.");
47461
+ }
47462
+ await rename(partialPath, destination);
47463
+ return { bytes: downloadedBytes };
47464
+ } catch (error48) {
47465
+ await file2.close().catch(() => void 0);
47466
+ await rm(partialPath, { force: true });
47467
+ if (error48 instanceof PentestDeliverableDownloadError) throw error48;
47468
+ const detail = error48 instanceof Error ? error48.message : "unknown error";
47469
+ throw new PentestDeliverableDownloadError(`Download from storage failed: ${detail}.`);
47470
+ }
47471
+ }
47472
+ async function writeAll(file2, bytes) {
47473
+ let offset = 0;
47474
+ while (offset < bytes.byteLength) {
47475
+ const { bytesWritten } = await file2.write(bytes, offset, bytes.byteLength - offset);
47476
+ if (bytesWritten === 0) {
47477
+ throw new PentestDeliverableDownloadError("Writing the document to disk stalled.");
47478
+ }
47479
+ offset += bytesWritten;
47480
+ }
47481
+ }
47482
+ var DELIVERABLE_LABEL = {
47483
+ report: "report",
47484
+ attestation: "letter of attestation"
47485
+ };
47486
+ function requirePentestIdentifier(value) {
47487
+ if (!isPentestIdentifier(value)) {
47488
+ console.error(
47489
+ colorError(`Invalid pentest identifier '${value}'. Use a display ID (e.g. PT-3) or UUID.`)
47490
+ );
47491
+ process.exit(1);
47492
+ }
47493
+ }
47494
+ function failDeliverable(status, body, deliverable, pentestIdentifier, action) {
47495
+ const { message, errorCode } = readErrorFields(body);
47496
+ const label = DELIVERABLE_LABEL[deliverable];
47497
+ if (status === UNAUTHORIZED_STATUS2) {
47498
+ exitAuthFailed();
47499
+ }
47500
+ if (status === 403) {
47501
+ console.error(
47502
+ colorError(
47503
+ message ?? `You do not have permission to ${action === "list" ? "view" : "download"} ${label}s for this organization.`
47504
+ )
47505
+ );
47506
+ process.exit(1);
47507
+ }
47508
+ if (status === 404) {
47509
+ if (message === "Report not found") {
47510
+ console.error(
47511
+ colorError(
47512
+ `No report has been shared for pentest ${pentestIdentifier} yet. Trace publishes it to your dashboard when the engagement's report is ready.`
47513
+ )
47514
+ );
47515
+ process.exit(1);
47516
+ }
47517
+ if (message === "Attestation not found") {
47518
+ console.error(
47519
+ colorError(
47520
+ `That letter of attestation is not available for pentest ${pentestIdentifier}. Run \`tracecli pentest attestation list ${pentestIdentifier}\` to see the letters shared with you.`
47521
+ )
47522
+ );
47523
+ process.exit(1);
47524
+ }
47525
+ console.error(
47526
+ colorError(
47527
+ `Pentest '${pentestIdentifier}' not found. Verify the ID with \`tracecli pentest list\`.`
47528
+ )
47529
+ );
47530
+ process.exit(1);
47531
+ }
47532
+ if (status === 409) {
47533
+ if (errorCode === "DOCX_NOT_RENDERED") {
47534
+ console.error(
47535
+ colorError(
47536
+ "No Word document is available for this report. Drop --docx to download the PDF."
47537
+ )
47538
+ );
47539
+ process.exit(1);
47540
+ }
47541
+ console.error(colorError(message ?? `Conflict (status ${status}).`));
47542
+ process.exit(1);
47543
+ }
47544
+ if (status === 400) {
47545
+ console.error(colorError(message ?? "Invalid request (status 400)."));
47546
+ process.exit(1);
47547
+ }
47548
+ console.error(
47549
+ colorError(
47550
+ `Failed to ${action} ${label}${action === "list" ? "s" : ""} (status ${status})${message ? `: ${message}` : "."}`
47551
+ )
47552
+ );
47553
+ process.exit(1);
47554
+ }
47555
+ function writeDownloadProgress(progress3) {
47556
+ if (!process.stderr.isTTY) return;
47557
+ const downloaded = formatArchiveBytes(progress3.downloadedBytes);
47558
+ if (progress3.totalBytes === null || progress3.totalBytes === 0) {
47559
+ process.stderr.write(`\rDownloading ${downloaded}`);
47560
+ return;
47561
+ }
47562
+ const pct = Math.floor(progress3.downloadedBytes / progress3.totalBytes * 100);
47563
+ process.stderr.write(
47564
+ `\rDownloading ${pct}% (${downloaded} / ${formatArchiveBytes(progress3.totalBytes)})`
47565
+ );
47566
+ }
47567
+ function finishProgress() {
47568
+ if (process.stderr.isTTY) process.stderr.write("\n");
47569
+ }
47570
+ async function saveDeliverable(downloadUrl, fileName, options) {
47571
+ const destination = await resolveDeliverableDestination(fileName, options.out);
47572
+ if (!options.force && await pathExists(destination)) {
47573
+ console.error(
47574
+ colorError(`${destination} already exists. Pass --force to overwrite it or --out <path>.`)
47575
+ );
47576
+ process.exit(1);
47577
+ }
47578
+ if (options.output === "table") {
47579
+ console.error(`Downloading ${sanitizeDisplay(fileName)} to ${destination}`);
47580
+ }
47581
+ try {
47582
+ const { bytes } = await downloadDeliverableToFile(downloadUrl, destination, {
47583
+ onProgress: writeDownloadProgress
47584
+ });
47585
+ finishProgress();
47586
+ return { destination, bytes };
47587
+ } catch (error48) {
47588
+ finishProgress();
47589
+ const detail = error48 instanceof Error ? error48.message : "Download from storage failed.";
47590
+ console.error(colorError(detail));
47591
+ process.exit(1);
47592
+ }
47593
+ }
47594
+ async function runReportDownload(pentestIdentifier, options) {
47595
+ const output = parseOutput(options.output);
47596
+ requirePentestIdentifier(pentestIdentifier);
47597
+ const artifact = options.docx ? "docx" : "primary";
47598
+ const organizationId = await resolveOrganizationId();
47599
+ const result = await authenticatedRequest(
47600
+ (client) => client.pentestReportPdf.downloadCustomer({
47601
+ params: { organizationId, pentestId: pentestIdentifier },
47602
+ query: {
47603
+ disposition: "attachment",
47604
+ ...artifact === "docx" ? { artifact: "docx" } : {}
47605
+ }
47606
+ })
47607
+ );
47608
+ if (result.status !== SUCCESS_STATUS) {
47609
+ failDeliverable(result.status, result.body, "report", pentestIdentifier, "download");
47610
+ }
47611
+ const { report, downloadUrl } = result.body;
47612
+ const fileName = reportFileName(report, artifact);
47613
+ const { destination, bytes } = await saveDeliverable(downloadUrl, fileName, {
47614
+ ...options.out !== void 0 ? { out: options.out } : {},
47615
+ ...options.force !== void 0 ? { force: options.force } : {},
47616
+ output
47617
+ });
47618
+ if (output === "json") {
47619
+ console.log(JSON.stringify({ report, artifact, path: destination, sizeBytes: bytes }, null, 2));
47620
+ return;
47621
+ }
47622
+ if (output === "tsv") {
47623
+ console.log(["documentId", "version", "artifact", "path", "sizeBytes"].join(" "));
47624
+ console.log(
47625
+ [
47626
+ tsvSafe(sanitizeDisplay(report.documentId)),
47627
+ tsvSafe(sanitizeDisplay(report.version)),
47628
+ artifact,
47629
+ tsvSafe(destination),
47630
+ String(bytes)
47631
+ ].join(" ")
47632
+ );
47633
+ return;
47634
+ }
47635
+ console.log(
47636
+ colorSuccess(
47637
+ `Saved ${sanitizeDisplay(report.documentId)} (${sanitizeDisplay(report.version)}) to ${destination} (${formatArchiveBytes(bytes)}).`
47638
+ )
47639
+ );
47640
+ }
47641
+ function createReportDownloadCommand() {
47642
+ return new Command("download").description("Download the pentest report shared with your organization").argument("<pentestId>", "Pentest display ID (e.g. PT-3) or UUID").option("--out <path>", "File to write, or an existing directory (default: working directory)").option("-f, --force", "Overwrite an existing file").option("--docx", "Download the Word document instead of the PDF (when your report ships one)").option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").addHelpText(
47643
+ "after",
47644
+ `
47645
+ Downloads the report Trace has published for the engagement: the issued version
47646
+ when one exists, otherwise the current shared draft. The file is named after the
47647
+ report's document ID (for example TR-PT-2026-03-ACME.pdf). Organizations on
47648
+ a partner report template receive a zip carrying the PDF, the Word document, the
47649
+ markdown report, and evidence files.
47650
+
47651
+ Works with a browser session from \`tracecli auth login\` or a Trace API key
47652
+ (TRACE_API_KEY), so it can run unattended in CI.
47653
+
47654
+ Examples:
47655
+ tracecli pentest report download PT-3
47656
+ tracecli pentest report download PT-3 --out ./reports/
47657
+ tracecli pentest report download PT-3 --out ./acme-q3.pdf --force
47658
+ tracecli pentest report download PT-3 --docx
47659
+ tracecli pentest report download PT-3 --output json | jq '.path'`
47660
+ ).action(async (pentestId, options) => {
47661
+ await runReportDownload(pentestId, options);
47662
+ });
47663
+ }
47664
+ function createReportCommand() {
47665
+ const report = new Command("report").description("Download the pentest report");
47666
+ report.addCommand(createReportDownloadCommand());
47667
+ return report;
47668
+ }
47669
+ function sortAttestationsNewestFirst(attestations) {
47670
+ return [...attestations].sort((a2, b) => {
47671
+ const delta = new Date(b.createdAt).getTime() - new Date(a2.createdAt).getTime();
47672
+ return delta !== 0 ? delta : a2.attestationId.localeCompare(b.attestationId);
47673
+ });
47674
+ }
47675
+ function renderAttestationLines(attestation) {
47676
+ const counts = attestation.severityCounts;
47677
+ return [
47678
+ ` ${sanitizeDisplay(attestation.documentId)} (${sanitizeDisplay(attestation.version)}, ${sanitizeDisplay(attestation.classification)})`,
47679
+ ` Issued: ${formatDate(attestation.issuedDate)} | Findings: ${attestation.totalFindings} (C${counts.critical} H${counts.high} M${counts.medium} L${counts.low} I${counts.info}) | ${formatArchiveBytes(attestation.fileSizeBytes)}`,
47680
+ ` ID: ${attestation.attestationId}`,
47681
+ ""
47682
+ ];
47683
+ }
47684
+ var ATTESTATION_TSV_HEADER = [
47685
+ "attestationId",
47686
+ "documentId",
47687
+ "version",
47688
+ "classification",
47689
+ "issuedDate",
47690
+ "totalFindings",
47691
+ "fileSizeBytes",
47692
+ "createdAt"
47693
+ ].join(" ");
47694
+ function attestationTsvRow(attestation) {
47695
+ return [
47696
+ attestation.attestationId,
47697
+ tsvSafe(sanitizeDisplay(attestation.documentId)),
47698
+ tsvSafe(sanitizeDisplay(attestation.version)),
47699
+ tsvSafe(sanitizeDisplay(attestation.classification)),
47700
+ attestation.issuedDate,
47701
+ String(attestation.totalFindings),
47702
+ String(attestation.fileSizeBytes),
47703
+ toIso(attestation.createdAt)
47704
+ ].join(" ");
47705
+ }
47706
+ async function fetchAttestations(organizationId, pentestIdentifier) {
47707
+ const result = await authenticatedRequest(
47708
+ (client) => client.pentestAttestation.listCustomerAttestations({
47709
+ params: { organizationId, pentestId: pentestIdentifier }
47710
+ })
47711
+ );
47712
+ if (result.status !== SUCCESS_STATUS) {
47713
+ failDeliverable(result.status, result.body, "attestation", pentestIdentifier, "list");
47714
+ }
47715
+ return sortAttestationsNewestFirst(result.body.attestations);
47716
+ }
47717
+ async function runAttestationList(pentestIdentifier, options) {
47718
+ const output = parseOutput(options.output);
47719
+ requirePentestIdentifier(pentestIdentifier);
47720
+ const organizationId = await resolveOrganizationId();
47721
+ const attestations = await fetchAttestations(organizationId, pentestIdentifier);
47722
+ if (output === "json") {
47723
+ console.log(JSON.stringify(attestations, null, 2));
47724
+ return;
47725
+ }
47726
+ if (output === "tsv") {
47727
+ console.log(ATTESTATION_TSV_HEADER);
47728
+ for (const attestation of attestations) {
47729
+ console.log(attestationTsvRow(attestation));
47730
+ }
47731
+ return;
47732
+ }
47733
+ if (attestations.length === 0) {
47734
+ console.log(`No letters of attestation have been shared for pentest ${pentestIdentifier}.`);
47735
+ return;
47736
+ }
47737
+ const header = `Found ${attestations.length} letter${attestations.length === 1 ? "" : "s"} of attestation (newest first):`;
47738
+ await runInteractivePager({
47739
+ items: attestations,
47740
+ header,
47741
+ renderItem: (attestation) => renderAttestationLines(attestation),
47742
+ disabled: options.pager === false
47743
+ });
47744
+ }
47745
+ function createAttestationListCommand() {
47746
+ return new Command("list").description("List the letters of attestation shared for a pentest").argument("<pentestId>", "Pentest display ID (e.g. PT-3) or UUID").option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").option("--no-pager", "Disable the interactive arrow-key pager for table output").addHelpText(
47747
+ "after",
47748
+ `
47749
+ Works with a browser session from \`tracecli auth login\` or a Trace API key
47750
+ (TRACE_API_KEY).
47751
+
47752
+ Examples:
47753
+ tracecli pentest attestation list PT-3
47754
+ tracecli pentest attestation list PT-3 --output json | jq '.[0].attestationId'
47755
+ tracecli pentest attestation list PT-3 --output tsv | cut -f1,2`
47756
+ ).action(async (pentestId, options) => {
47757
+ await runAttestationList(pentestId, options);
47758
+ });
47759
+ }
47760
+ async function runAttestationDownload(pentestIdentifier, options) {
47761
+ const output = parseOutput(options.output);
47762
+ requirePentestIdentifier(pentestIdentifier);
47763
+ if (options.attestation !== void 0 && !isUuid(options.attestation)) {
47764
+ console.error(
47765
+ colorError(
47766
+ `Invalid --attestation '${options.attestation}'. Pass the UUID shown by \`tracecli pentest attestation list ${pentestIdentifier}\`.`
47767
+ )
47768
+ );
47769
+ process.exit(1);
47770
+ }
47771
+ const organizationId = await resolveOrganizationId();
47772
+ let attestationId = options.attestation;
47773
+ if (attestationId === void 0) {
47774
+ const attestations = await fetchAttestations(organizationId, pentestIdentifier);
47775
+ const newest = attestations[0];
47776
+ if (newest === void 0) {
47777
+ console.error(
47778
+ colorError(
47779
+ `No letters of attestation have been shared for pentest ${pentestIdentifier} yet.`
47780
+ )
47781
+ );
47782
+ process.exit(1);
47783
+ }
47784
+ if (attestations.length > 1 && output === "table") {
47785
+ console.error(
47786
+ `Downloading the newest of ${attestations.length} letters (${sanitizeDisplay(newest.documentId)}). Pass --attestation <id> for another.`
47787
+ );
47788
+ }
47789
+ attestationId = newest.attestationId;
47790
+ }
47791
+ const result = await authenticatedRequest(
47792
+ (client) => client.pentestAttestation.downloadCustomerAttestation({
47793
+ params: { organizationId, pentestId: pentestIdentifier, attestationId },
47794
+ query: { disposition: "attachment" }
47795
+ })
47796
+ );
47797
+ if (result.status !== SUCCESS_STATUS) {
47798
+ failDeliverable(result.status, result.body, "attestation", pentestIdentifier, "download");
47799
+ }
47800
+ const { attestation, downloadUrl } = result.body;
47801
+ const fileName = attestationFileName(attestation);
47802
+ const { destination, bytes } = await saveDeliverable(downloadUrl, fileName, {
47803
+ ...options.out !== void 0 ? { out: options.out } : {},
47804
+ ...options.force !== void 0 ? { force: options.force } : {},
47805
+ output
47806
+ });
47807
+ if (output === "json") {
47808
+ console.log(JSON.stringify({ attestation, path: destination, sizeBytes: bytes }, null, 2));
47809
+ return;
47810
+ }
47811
+ if (output === "tsv") {
47812
+ console.log(["attestationId", "documentId", "version", "path", "sizeBytes"].join(" "));
47813
+ console.log(
47814
+ [
47815
+ attestation.attestationId,
47816
+ tsvSafe(sanitizeDisplay(attestation.documentId)),
47817
+ tsvSafe(sanitizeDisplay(attestation.version)),
47818
+ tsvSafe(destination),
47819
+ String(bytes)
47820
+ ].join(" ")
47821
+ );
47822
+ return;
47823
+ }
47824
+ console.log(
47825
+ colorSuccess(
47826
+ `Saved ${sanitizeDisplay(attestation.documentId)} (${sanitizeDisplay(attestation.version)}) to ${destination} (${formatArchiveBytes(bytes)}).`
47827
+ )
47828
+ );
47829
+ }
47830
+ function createAttestationDownloadCommand() {
47831
+ return new Command("download").description("Download a letter of attestation (newest by default)").argument("<pentestId>", "Pentest display ID (e.g. PT-3) or UUID").option(
47832
+ "--attestation <id>",
47833
+ "Attestation UUID from `pentest attestation list` (default: the newest letter)"
47834
+ ).option("--out <path>", "File to write, or an existing directory (default: working directory)").option("-f, --force", "Overwrite an existing file").option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").addHelpText(
47835
+ "after",
47836
+ `
47837
+ The file is named after the letter's document ID (for example
47838
+ TR-LOA-2026-03-ACME-1A2B3C.pdf). Works with a browser session from \`tracecli auth
47839
+ login\` or a Trace API key (TRACE_API_KEY), so it can run unattended in CI.
47840
+
47841
+ Examples:
47842
+ tracecli pentest attestation download PT-3
47843
+ tracecli pentest attestation download PT-3 --out ./compliance/
47844
+ tracecli pentest attestation download PT-3 --attestation 6f1c... --out ./loa.pdf --force
47845
+ tracecli pentest attestation download PT-3 --output json | jq '.path'`
47846
+ ).action(async (pentestId, options) => {
47847
+ await runAttestationDownload(pentestId, options);
47848
+ });
47849
+ }
47850
+ function createAttestationCommand() {
47851
+ const attestation = new Command("attestation").alias("loa").description("List and download letters of attestation");
47852
+ attestation.addCommand(createAttestationListCommand());
47853
+ attestation.addCommand(createAttestationDownloadCommand());
47854
+ return attestation;
47855
+ }
47856
+
47857
+ // src/pentest/evidence.ts
47858
+ import { readFileSync as readFileSync6, statSync as statSync4 } from "fs";
47859
+ import { basename as basename4 } from "path";
46315
47860
  var SECTION_LIST = ValidationEvidenceSectionValues.join(", ");
46316
47861
  var KIND_LIST = PentestExploitArtifactKindValues.join(", ");
46317
47862
  function isSection(value) {
@@ -46373,7 +47918,7 @@ async function runAttachEvidence(options) {
46373
47918
  }
46374
47919
  let sizeBytes;
46375
47920
  try {
46376
- const stats = statSync3(options.file);
47921
+ const stats = statSync4(options.file);
46377
47922
  if (!stats.isFile()) {
46378
47923
  console.error(colorError(`--file '${sanitizeDisplay(options.file)}' is not a regular file.`));
46379
47924
  process.exit(1);
@@ -46399,7 +47944,7 @@ async function runAttachEvidence(options) {
46399
47944
  const pentestId = await resolvePentestId(organizationId, options.pentest);
46400
47945
  const pentestFindingId = await resolveFindingId(organizationId, pentestId, options.finding);
46401
47946
  const params = { organizationId, pentestId, pentestFindingId };
46402
- const filename = basename2(options.file);
47947
+ const filename = basename4(options.file);
46403
47948
  const slot = await authenticatedRequest(
46404
47949
  (client) => client.pentestFinding.createFindingEvidenceUpload({
46405
47950
  params,
@@ -46485,193 +48030,13 @@ Examples:
46485
48030
  });
46486
48031
  }
46487
48032
 
46488
- // src/pentest/status-update.ts
46489
- var PENTEST_IDENTIFIER_PATTERN = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|pt-\d+|\d+)$/i;
46490
- var FINDING_IDENTIFIER_PATTERN = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|ptf-\d+|\d+)$/i;
46491
- var MAX_NOTE_LENGTH = 1e4;
46492
- function parseStatusUpdateTarget(raw) {
46493
- const normalized = raw.trim().toUpperCase().replace(/[\s-]+/g, "_");
46494
- if (normalized === "IN_REVIEW" || normalized === "REVIEW") {
46495
- return {
46496
- error: "IN_REVIEW is not a finding status. Use IN_PROGRESS while fixing, then REMEDIATED when the fix is ready for retest."
46497
- };
46498
- }
46499
- if (normalized === "IN_PROGRESS" || normalized === "REMEDIATED") {
46500
- return normalized;
46501
- }
46502
- return {
46503
- error: `Invalid --status '${sanitizeDisplay(raw)}'. Use IN_PROGRESS or REMEDIATED.`
46504
- };
46505
- }
46506
- function isPentestIdentifier(value) {
46507
- return PENTEST_IDENTIFIER_PATTERN.test(value);
46508
- }
46509
- function isFindingIdentifier(value) {
46510
- return FINDING_IDENTIFIER_PATTERN.test(value);
46511
- }
46512
- function failStatusUpdate(status, body, viaApiKey) {
46513
- const { message, errorCode } = readErrorFields(body);
46514
- if (status === UNAUTHORIZED_STATUS2) {
46515
- exitAuthFailed();
46516
- }
46517
- if (status === 403 && viaApiKey) {
46518
- console.error(
46519
- colorError(
46520
- `${message ?? "Request failed (status 403)."}
46521
- API keys can update finding status once Trace enables finding automation for your organization. Ask Trace to turn it on, or run this with \`tracecli auth login\`.`
46522
- )
46523
- );
46524
- process.exit(1);
46525
- }
46526
- if (status === 409 && errorCode === "FINDING_STALE") {
46527
- console.error(
46528
- colorError(message ?? "The finding changed while this update was in flight. Retry it.")
46529
- );
46530
- process.exit(1);
46531
- }
46532
- if (status === 404) {
46533
- console.error(colorError(message ?? "Pentest or finding not found."));
46534
- process.exit(1);
46535
- }
46536
- if (status === 409) {
46537
- if (errorCode === "PENTEST_CLOSED") {
46538
- console.error(
46539
- colorError(message ?? "Closed pentests must be reopened before updating findings.")
46540
- );
46541
- } else if (errorCode === "FINDING_TRANSITION_NOT_ALLOWED") {
46542
- console.error(
46543
- colorError(
46544
- message ?? "That status change is not allowed from the finding's current status."
46545
- )
46546
- );
46547
- } else {
46548
- console.error(colorError(message ?? "Conflict (status 409)."));
46549
- }
46550
- process.exit(1);
46551
- }
46552
- if (status === 400 || status === 403) {
46553
- console.error(colorError(message ?? `Request failed (status ${status}).`));
46554
- process.exit(1);
46555
- }
46556
- console.error(
46557
- colorError(
46558
- `Failed to update finding status (status ${status})${message ? `: ${message}` : "."}`
46559
- )
46560
- );
46561
- process.exit(1);
46562
- }
46563
- async function runStatusUpdate(options) {
46564
- if (options.output !== "text" && options.output !== "json") {
46565
- console.error(
46566
- colorError(`Invalid --output '${sanitizeDisplay(options.output)}'. Use text or json.`)
46567
- );
46568
- process.exit(1);
46569
- }
46570
- const target = parseStatusUpdateTarget(options.status);
46571
- if (typeof target !== "string") {
46572
- console.error(colorError(target.error));
46573
- process.exit(1);
46574
- }
46575
- if (!isPentestIdentifier(options.pentest)) {
46576
- console.error(
46577
- colorError(
46578
- `Invalid pentest identifier '${sanitizeDisplay(options.pentest)}'. Use a display ID (e.g. PT-3) or UUID.`
46579
- )
46580
- );
46581
- process.exit(1);
46582
- }
46583
- if (!isFindingIdentifier(options.finding)) {
46584
- console.error(
46585
- colorError(
46586
- `Invalid finding identifier '${sanitizeDisplay(options.finding)}'. Use a finding number (e.g. PTF-7) or UUID.`
46587
- )
46588
- );
46589
- process.exit(1);
46590
- }
46591
- const note = options.note?.trim();
46592
- if (note !== void 0 && note.length === 0) {
46593
- console.error(colorError("--note cannot be empty."));
46594
- process.exit(1);
46595
- }
46596
- if (note !== void 0 && note.length > MAX_NOTE_LENGTH) {
46597
- console.error(
46598
- colorError(`--note is ${note.length} characters; the limit is ${MAX_NOTE_LENGTH}.`)
46599
- );
46600
- process.exit(1);
46601
- }
46602
- if (note !== void 0 && target === "IN_PROGRESS") {
46603
- console.error(
46604
- colorError("--note is only used when reporting remediation (--status REMEDIATED).")
46605
- );
46606
- process.exit(1);
46607
- }
46608
- const viaApiKey = requireAuth().kind === "api_key";
46609
- const organizationId = await resolveOrganizationId();
46610
- const params = {
46611
- organizationId,
46612
- pentestId: options.pentest,
46613
- pentestFindingId: options.finding
46614
- };
46615
- const result = viaApiKey ? await authenticatedRequest(
46616
- (client) => client.pentestFinding.automationStatusUpdate({
46617
- params,
46618
- body: { status: target, ...note !== void 0 ? { note } : {} }
46619
- })
46620
- ) : target === "IN_PROGRESS" ? await authenticatedRequest(
46621
- (client) => client.pentestFinding.startProgress({ params, body: {} })
46622
- ) : await authenticatedRequest(
46623
- (client) => client.pentestFinding.reportRemediation({
46624
- params,
46625
- body: note !== void 0 ? { note } : {}
46626
- })
46627
- );
46628
- if (result.status !== 200) {
46629
- failStatusUpdate(result.status, result.body, viaApiKey);
46630
- }
46631
- if (options.output === "json") {
46632
- console.log(JSON.stringify(result.body, null, 2));
46633
- return;
46634
- }
46635
- const displayId = result.body.displayId;
46636
- const status = sanitizeDisplay(result.body.status);
46637
- console.log(colorSuccess(`PTF-${displayId} is now ${status}`));
46638
- }
46639
- function createStatusUpdateCommand() {
46640
- 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(
46641
- "--status <status>",
46642
- "IN_PROGRESS (start work) or REMEDIATED (ready for retest)"
46643
- ).option("--note <text>", "Optional context when reporting remediation").option("-o, --output <format>", "Output format: text, json (default: text)", "text").addHelpText(
46644
- "after",
46645
- `
46646
- Customer-facing status writes \u2014 the same transitions as the dashboard. An agent
46647
- can mark a finding in progress when it starts the fix, then remediated when the
46648
- fix is ready for Trace to re-test. Leaving the finding IN_PROGRESS (and not
46649
- sending REMEDIATED) keeps it out of the retest queue.
46650
-
46651
- IN_PROGRESS is allowed from OPEN or VERIFIED_OPEN. REMEDIATED is allowed from
46652
- OPEN, IN_PROGRESS, or VERIFIED_OPEN.
46653
-
46654
- A signed-in session (\`tracecli auth login\`) attributes the change to you. A
46655
- Trace API key works too, once Trace enables finding automation for your
46656
- organization: those writes are recorded as \`api-key:<key name>\` and show up as
46657
- yours in the finding's history.
46658
-
46659
- Examples:
46660
- tracecli pentest findings status-update --pentest PT-3 --finding 7 --status IN_PROGRESS
46661
- tracecli pentest findings status-update --pentest PT-3 --finding PTF-7 --status REMEDIATED
46662
- tracecli pentest findings status-update --pentest PT-3 --finding 7 --status REMEDIATED \\
46663
- --note "Fixed in PR 412" --output json`
46664
- ).action(async (options) => {
46665
- await runStatusUpdate(options);
46666
- });
46667
- }
46668
-
46669
48033
  // src/pentest/get-finding.ts
46670
48034
  var OUTPUT_FORMATS2 = ["text", "json"];
46671
48035
  function isOutputFormat2(value) {
46672
48036
  return OUTPUT_FORMATS2.includes(value);
46673
48037
  }
46674
48038
  var PROSE_SECTIONS = [
48039
+ { key: "summary", heading: "Summary" },
46675
48040
  { key: "description", heading: "Description" },
46676
48041
  { key: "impact", heading: "Impact" },
46677
48042
  { key: "attackScenario", heading: "Attack Scenario" },
@@ -47776,12 +49141,14 @@ function createPentestCommand() {
47776
49141
  pentest.addCommand(createUpdateCommand4());
47777
49142
  pentest.addCommand(createQuestionnaireCommand());
47778
49143
  pentest.addCommand(createFindingsCommand());
49144
+ pentest.addCommand(createReportCommand());
49145
+ pentest.addCommand(createAttestationCommand());
47779
49146
  return pentest;
47780
49147
  }
47781
49148
 
47782
49149
  // src/update/commands.ts
47783
49150
  import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
47784
- import { dirname as dirname3, join as join6 } from "path";
49151
+ import { dirname as dirname3, join as join7 } from "path";
47785
49152
  import { createInterface as createInterface4 } from "readline/promises";
47786
49153
  var UPDATE_CACHE_FILE = "update-check.json";
47787
49154
  var UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1e3;
@@ -47790,7 +49157,7 @@ var DISABLE_UPDATE_NOTIFIER_ENV = "TRACECLI_DISABLE_UPDATE_NOTIFIER";
47790
49157
  var REGISTRY_URL_ENV = "TRACECLI_NPM_REGISTRY_URL";
47791
49158
  var noticePrinted = false;
47792
49159
  function getUpdateCheckCachePath() {
47793
- return join6(getTraceDir(), UPDATE_CACHE_FILE);
49160
+ return join7(getTraceDir(), UPDATE_CACHE_FILE);
47794
49161
  }
47795
49162
  function readUpdateCheckCache() {
47796
49163
  const cachePath = getUpdateCheckCachePath();
@@ -48164,153 +49531,8 @@ Examples:
48164
49531
 
48165
49532
  // src/upload/download.ts
48166
49533
  import { createHash as createHash2 } from "crypto";
48167
- import { open as open2, rename, rm, stat } from "fs/promises";
48168
- import { basename as basename4, join as join7, resolve } from "path";
48169
-
48170
- // src/upload/prepare.ts
48171
- import { createHash } from "crypto";
48172
- import { createReadStream, statSync as statSync4 } from "fs";
48173
- import { open } from "fs/promises";
48174
- import { basename as basename3 } from "path";
48175
- var CodeUploadPrepareError = class extends Error {
48176
- constructor(message) {
48177
- super(message);
48178
- this.name = "CodeUploadPrepareError";
48179
- }
48180
- };
48181
- var KIND_BY_INPUT = {
48182
- source: "SOURCE_ARCHIVE",
48183
- code: "SOURCE_ARCHIVE",
48184
- "source-archive": "SOURCE_ARCHIVE",
48185
- source_archive: "SOURCE_ARCHIVE",
48186
- SOURCE_ARCHIVE: "SOURCE_ARCHIVE",
48187
- apk: "ANDROID_APK",
48188
- android: "ANDROID_APK",
48189
- "android-apk": "ANDROID_APK",
48190
- android_apk: "ANDROID_APK",
48191
- ANDROID_APK: "ANDROID_APK",
48192
- ipa: "IOS_IPA",
48193
- ios: "IOS_IPA",
48194
- "ios-ipa": "IOS_IPA",
48195
- ios_ipa: "IOS_IPA",
48196
- IOS_IPA: "IOS_IPA"
48197
- };
48198
- function normalizeArtifactKind(input) {
48199
- const trimmed = input.trim();
48200
- return KIND_BY_INPUT[trimmed] ?? KIND_BY_INPUT[trimmed.toLowerCase()] ?? null;
48201
- }
48202
- function inferArtifactKindFromFilename(filename) {
48203
- const lower = filename.toLowerCase();
48204
- if (lower.endsWith(".zip")) return "SOURCE_ARCHIVE";
48205
- if (lower.endsWith(".apk")) return "ANDROID_APK";
48206
- if (lower.endsWith(".ipa")) return "IOS_IPA";
48207
- return null;
48208
- }
48209
- function requiredExtensionForKind(kind) {
48210
- if (kind === "ANDROID_APK") return ".apk";
48211
- if (kind === "IOS_IPA") return ".ipa";
48212
- return ".zip";
48213
- }
48214
- function defaultDisplayName(originalFilename) {
48215
- const base = originalFilename.replace(/\.(zip|apk|ipa)$/i, "").trim();
48216
- return base.slice(0, 200) || "source";
48217
- }
48218
- function formatArchiveBytes(bytes) {
48219
- if (bytes < 1024) return `${bytes} B`;
48220
- if (bytes < 1024 * 1024) {
48221
- const kb = bytes / 1024;
48222
- return `${kb.toFixed(Number.isInteger(kb) ? 0 : 1)} KB`;
48223
- }
48224
- if (bytes < 1024 * 1024 * 1024) {
48225
- const mb = bytes / (1024 * 1024);
48226
- return `${mb.toFixed(Number.isInteger(mb) ? 0 : 1)} MB`;
48227
- }
48228
- const gb = bytes / (1024 * 1024 * 1024);
48229
- return `${gb.toFixed(Number.isInteger(gb) ? 0 : 1)} GB`;
48230
- }
48231
- function archiveTooLargeMessage() {
48232
- return `The file must be ${formatArchiveBytes(CODE_UPLOAD_MAX_BYTES)} or smaller.`;
48233
- }
48234
- async function hashFileSha256(filePath) {
48235
- const hash2 = createHash("sha256");
48236
- const stream = createReadStream(filePath);
48237
- for await (const chunk of stream) {
48238
- hash2.update(chunk);
48239
- }
48240
- return hash2.digest("hex");
48241
- }
48242
- async function readFileSlice(filePath, start, endExclusive) {
48243
- const length = endExclusive - start;
48244
- if (!Number.isSafeInteger(start) || !Number.isSafeInteger(length) || start < 0 || length < 1) {
48245
- throw new Error(
48246
- `Invalid file slice: start=${String(start)} endExclusive=${String(endExclusive)}.`
48247
- );
48248
- }
48249
- const buffer = Buffer.alloc(length);
48250
- const handle = await open(filePath, "r");
48251
- try {
48252
- const { bytesRead } = await handle.read(buffer, 0, length, start);
48253
- if (bytesRead !== length) {
48254
- throw new Error(
48255
- `Expected to read ${String(length)} bytes at offset ${String(start)}, got ${String(bytesRead)}.`
48256
- );
48257
- }
48258
- return buffer;
48259
- } finally {
48260
- await handle.close();
48261
- }
48262
- }
48263
- function assertPreparedMatchesUpload(upload, prepared) {
48264
- if (upload.sizeBytes !== prepared.sizeBytes || upload.archiveSha256 !== prepared.archiveSha256) {
48265
- throw new CodeUploadPrepareError(
48266
- "The file does not match the interrupted upload (size or SHA-256 differs)."
48267
- );
48268
- }
48269
- }
48270
- async function prepareUploadFile(filePath, options = {}) {
48271
- let sizeBytes;
48272
- try {
48273
- const stats = statSync4(filePath);
48274
- if (!stats.isFile()) {
48275
- throw new CodeUploadPrepareError("Path is not a regular file. Pass a .zip, .apk, or .ipa.");
48276
- }
48277
- sizeBytes = stats.size;
48278
- } catch (error48) {
48279
- if (error48 instanceof CodeUploadPrepareError) throw error48;
48280
- throw new CodeUploadPrepareError("Cannot read that file.");
48281
- }
48282
- if (!Number.isSafeInteger(sizeBytes) || sizeBytes < 1) {
48283
- throw new CodeUploadPrepareError("The file is empty.");
48284
- }
48285
- if (sizeBytes > CODE_UPLOAD_MAX_BYTES) {
48286
- throw new CodeUploadPrepareError(archiveTooLargeMessage());
48287
- }
48288
- const originalFilename = basename3(filePath);
48289
- const inferred = inferArtifactKindFromFilename(originalFilename);
48290
- const artifactKind = options.artifactKind ?? inferred;
48291
- if (artifactKind == null) {
48292
- throw new CodeUploadPrepareError("Pass a .zip source archive, an .apk, or an .ipa.");
48293
- }
48294
- const expected = requiredExtensionForKind(artifactKind);
48295
- if (!originalFilename.toLowerCase().endsWith(expected)) {
48296
- const label = isApplicationBuildArtifactKind(artifactKind) ? artifactKind === "ANDROID_APK" ? "Android APK" : "iOS package" : "source archive";
48297
- throw new CodeUploadPrepareError(`A ${label} must end in ${expected}.`);
48298
- }
48299
- const displayName = (options.displayName ?? defaultDisplayName(originalFilename)).trim();
48300
- if (displayName.length < 1 || displayName.length > 200) {
48301
- throw new CodeUploadPrepareError("Display name must be 1 to 200 characters.");
48302
- }
48303
- return {
48304
- path: filePath,
48305
- displayName,
48306
- originalFilename,
48307
- sizeBytes,
48308
- archiveSha256: await hashFileSha256(filePath),
48309
- artifactKind
48310
- };
48311
- }
48312
-
48313
- // src/upload/download.ts
49534
+ import { open as open3, rename as rename2, rm as rm2, stat as stat2 } from "fs/promises";
49535
+ import { basename as basename5, join as join8, resolve as resolve2 } from "path";
48314
49536
  var CodeUploadDownloadError = class extends Error {
48315
49537
  constructor(message) {
48316
49538
  super(message);
@@ -48320,7 +49542,7 @@ var CodeUploadDownloadError = class extends Error {
48320
49542
  function safeDownloadFilename(upload) {
48321
49543
  const extension = requiredExtensionForKind(upload.artifactKind);
48322
49544
  const fallback = `app${extension}`;
48323
- const stripped = Array.from(basename4(upload.originalFilename.replace(/\\/g, "/")), (char) => {
49545
+ const stripped = Array.from(basename5(upload.originalFilename.replace(/\\/g, "/")), (char) => {
48324
49546
  const code = char.codePointAt(0) ?? 0;
48325
49547
  return code <= 31 || code === 127 ? "_" : char;
48326
49548
  }).join("").trim();
@@ -48329,18 +49551,18 @@ function safeDownloadFilename(upload) {
48329
49551
  }
48330
49552
  async function resolveDownloadDestination(upload, outOption) {
48331
49553
  const filename = safeDownloadFilename(upload);
48332
- if (outOption === void 0) return resolve(process.cwd(), filename);
48333
- const target = resolve(outOption);
49554
+ if (outOption === void 0) return resolve2(process.cwd(), filename);
49555
+ const target = resolve2(outOption);
48334
49556
  try {
48335
- const info = await stat(target);
48336
- if (info.isDirectory()) return join7(target, filename);
49557
+ const info = await stat2(target);
49558
+ if (info.isDirectory()) return join8(target, filename);
48337
49559
  } catch {
48338
49560
  }
48339
49561
  return target;
48340
49562
  }
48341
- async function pathExists(path) {
49563
+ async function pathExists2(path) {
48342
49564
  try {
48343
- await stat(path);
49565
+ await stat2(path);
48344
49566
  return true;
48345
49567
  } catch {
48346
49568
  return false;
@@ -48372,8 +49594,8 @@ async function downloadCodeUploadToFile(upload, downloadUrl, destination, option
48372
49594
  );
48373
49595
  }
48374
49596
  }
48375
- await rm(partialPath, { force: true });
48376
- const file2 = await open2(partialPath, "wx");
49597
+ await rm2(partialPath, { force: true });
49598
+ const file2 = await open3(partialPath, "wx");
48377
49599
  const hash2 = createHash2("sha256");
48378
49600
  const reader = response.body.getReader();
48379
49601
  let downloadedBytes = 0;
@@ -48390,7 +49612,7 @@ async function downloadCodeUploadToFile(upload, downloadUrl, destination, option
48390
49612
  );
48391
49613
  }
48392
49614
  hash2.update(chunk.value);
48393
- await writeAll(file2, chunk.value);
49615
+ await writeAll2(file2, chunk.value);
48394
49616
  options.onProgress?.(progress(downloadedBytes, upload.sizeBytes));
48395
49617
  }
48396
49618
  await file2.close();
@@ -48405,10 +49627,10 @@ async function downloadCodeUploadToFile(upload, downloadUrl, destination, option
48405
49627
  "Downloaded bytes do not match the SHA-256 recorded at upload time."
48406
49628
  );
48407
49629
  }
48408
- await rename(partialPath, destination);
49630
+ await rename2(partialPath, destination);
48409
49631
  } catch (error48) {
48410
49632
  await file2.close().catch(() => void 0);
48411
- await rm(partialPath, { force: true });
49633
+ await rm2(partialPath, { force: true });
48412
49634
  if (error48 instanceof CodeUploadDownloadError) throw error48;
48413
49635
  const detail = error48 instanceof Error ? error48.message : "unknown error";
48414
49636
  throw new CodeUploadDownloadError(`Download from storage failed: ${detail}.`);
@@ -48421,7 +49643,7 @@ function progress(downloadedBytes, totalBytes) {
48421
49643
  fraction: totalBytes === 0 ? 1 : downloadedBytes / totalBytes
48422
49644
  };
48423
49645
  }
48424
- async function writeAll(file2, bytes) {
49646
+ async function writeAll2(file2, bytes) {
48425
49647
  let offset = 0;
48426
49648
  while (offset < bytes.byteLength) {
48427
49649
  const { bytesWritten } = await file2.write(bytes, offset, bytes.byteLength - offset);
@@ -48557,13 +49779,13 @@ function progress2(uploadedBytes, totalBytes) {
48557
49779
  };
48558
49780
  }
48559
49781
  function defaultWait(milliseconds) {
48560
- return new Promise((resolve2) => setTimeout(resolve2, milliseconds));
49782
+ return new Promise((resolve3) => setTimeout(resolve3, milliseconds));
48561
49783
  }
48562
49784
 
48563
49785
  // src/upload/commands.ts
48564
49786
  var TERMINAL_STATUSES = /* @__PURE__ */ new Set(["READY", "REJECTED", "FAILED", "DELETING"]);
48565
49787
  var DEFAULT_RUNTIME = {
48566
- wait: (milliseconds) => new Promise((resolve2) => setTimeout(resolve2, milliseconds)),
49788
+ wait: (milliseconds) => new Promise((resolve3) => setTimeout(resolve3, milliseconds)),
48567
49789
  pollIntervalMs: 3e3,
48568
49790
  fetch: (input, init) => globalThis.fetch(input, init)
48569
49791
  };
@@ -48819,14 +50041,14 @@ function writeProgress(progress3) {
48819
50041
  `\rUploading ${pct}% (${formatArchiveBytes(progress3.uploadedBytes)} / ${formatArchiveBytes(progress3.totalBytes)})`
48820
50042
  );
48821
50043
  }
48822
- function writeDownloadProgress(progress3) {
50044
+ function writeDownloadProgress2(progress3) {
48823
50045
  if (!process.stderr.isTTY) return;
48824
50046
  const pct = Math.floor(progress3.fraction * 100);
48825
50047
  process.stderr.write(
48826
50048
  `\rDownloading ${pct}% (${formatArchiveBytes(progress3.downloadedBytes)} / ${formatArchiveBytes(progress3.totalBytes)})`
48827
50049
  );
48828
50050
  }
48829
- function finishProgress() {
50051
+ function finishProgress2() {
48830
50052
  if (process.stderr.isTTY) process.stderr.write("\n");
48831
50053
  }
48832
50054
  async function listUploads(organizationId, query = {}) {
@@ -48956,9 +50178,9 @@ async function runTransfer(filePath, options, runtime) {
48956
50178
  refreshParts: () => refreshRemainingParts(organizationId, codeUploadId)
48957
50179
  }
48958
50180
  );
48959
- finishProgress();
50181
+ finishProgress2();
48960
50182
  } catch (error48) {
48961
- finishProgress();
50183
+ finishProgress2();
48962
50184
  const detail = error48 instanceof Error ? error48.message : "Upload to storage failed.";
48963
50185
  console.error(colorError(detail));
48964
50186
  console.error(resumeHint(codeUploadId, filePath));
@@ -49071,9 +50293,9 @@ async function runResume(codeUploadIdArg, filePath, options, runtime) {
49071
50293
  refreshParts: () => refreshRemainingParts(organizationId, codeUploadId)
49072
50294
  }
49073
50295
  );
49074
- finishProgress();
50296
+ finishProgress2();
49075
50297
  } catch (error48) {
49076
- finishProgress();
50298
+ finishProgress2();
49077
50299
  const detail = error48 instanceof Error ? error48.message : "Upload to storage failed.";
49078
50300
  console.error(colorError(detail));
49079
50301
  console.error(resumeHint(codeUploadId, filePath));
@@ -49151,7 +50373,7 @@ async function runDownload(codeUploadIdArg, options, runtime) {
49151
50373
  }
49152
50374
  const { upload, downloadUrl } = result.body;
49153
50375
  const destination = await resolveDownloadDestination(upload, options.out);
49154
- if (!options.force && await pathExists(destination)) {
50376
+ if (!options.force && await pathExists2(destination)) {
49155
50377
  console.error(
49156
50378
  colorError(`${destination} already exists. Pass --force to overwrite it or --out <path>.`)
49157
50379
  );
@@ -49165,11 +50387,11 @@ async function runDownload(codeUploadIdArg, options, runtime) {
49165
50387
  try {
49166
50388
  await downloadCodeUploadToFile(upload, downloadUrl, destination, {
49167
50389
  fetch: runtime.fetch,
49168
- onProgress: writeDownloadProgress
50390
+ onProgress: writeDownloadProgress2
49169
50391
  });
49170
- finishProgress();
50392
+ finishProgress2();
49171
50393
  } catch (error48) {
49172
- finishProgress();
50394
+ finishProgress2();
49173
50395
  const detail = error48 instanceof Error ? error48.message : "Download from storage failed.";
49174
50396
  console.error(colorError(detail));
49175
50397
  process.exit(1);