securewithtrace 0.46.0 → 0.47.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 +1278 -269
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -24693,6 +24693,10 @@ var PentestFindingRepositorySchema = external_exports.object({
|
|
|
24693
24693
|
startLine: external_exports.number().nullable(),
|
|
24694
24694
|
endLine: external_exports.number().nullable()
|
|
24695
24695
|
});
|
|
24696
|
+
var PentestFindingApplicationSchema = external_exports.object({
|
|
24697
|
+
applicationId: external_exports.uuid(),
|
|
24698
|
+
name: external_exports.string()
|
|
24699
|
+
});
|
|
24696
24700
|
var PentestFindingResponseSchema = external_exports.object({
|
|
24697
24701
|
pentestFindingId: external_exports.uuid(),
|
|
24698
24702
|
organizationId: external_exports.uuid(),
|
|
@@ -24756,6 +24760,10 @@ var PentestFindingListItemSchema = PentestFindingResponseSchema.omit({
|
|
|
24756
24760
|
attackScenario: true,
|
|
24757
24761
|
severityConsiderations: true,
|
|
24758
24762
|
verification: true
|
|
24763
|
+
}).extend({
|
|
24764
|
+
// Derived by `loadFindingApplications` for the list reads only; it backs the
|
|
24765
|
+
// application filter. `.default([])` keeps an API that predates it parsing.
|
|
24766
|
+
applications: external_exports.array(PentestFindingApplicationSchema).default([])
|
|
24759
24767
|
});
|
|
24760
24768
|
var ListPentestFindingsResponseSchema = external_exports.array(PentestFindingListItemSchema);
|
|
24761
24769
|
var OperatorFindingEvidenceSchema = external_exports.object({
|
|
@@ -24804,7 +24812,8 @@ var OperatorFindingListSchema = PentestFindingListItemSchema.omit({
|
|
|
24804
24812
|
createdByName: true,
|
|
24805
24813
|
reportProseAt: true,
|
|
24806
24814
|
reportProseStatus: true,
|
|
24807
|
-
externalTicket: true
|
|
24815
|
+
externalTicket: true,
|
|
24816
|
+
applications: true
|
|
24808
24817
|
});
|
|
24809
24818
|
var CustomerPentestFindingRepositorySchema = PentestFindingRepositorySchema.omit({
|
|
24810
24819
|
repositoryId: true
|
|
@@ -25086,6 +25095,30 @@ var PublishedFindingSchema = external_exports.object({
|
|
|
25086
25095
|
var PublishAllFindingsResponseSchema = external_exports.object({
|
|
25087
25096
|
published: external_exports.array(PublishedFindingSchema)
|
|
25088
25097
|
});
|
|
25098
|
+
var CopyPentestFindingsRequestSchema = external_exports.object({
|
|
25099
|
+
sourcePentestId: external_exports.uuid(),
|
|
25100
|
+
pentestFindingIds: external_exports.array(external_exports.uuid()).min(1).max(200).optional(),
|
|
25101
|
+
applicationId: external_exports.uuid().optional(),
|
|
25102
|
+
/** Report the selection without writing anything. */
|
|
25103
|
+
dryRun: external_exports.boolean().optional()
|
|
25104
|
+
}).strict().refine((body) => body.pentestFindingIds === void 0 !== (body.applicationId === void 0), {
|
|
25105
|
+
message: "Provide exactly one of pentestFindingIds or applicationId."
|
|
25106
|
+
});
|
|
25107
|
+
var CopiedPentestFindingSchema = external_exports.object({
|
|
25108
|
+
sourcePentestFindingId: external_exports.uuid(),
|
|
25109
|
+
sourceDisplayId: external_exports.int().nullable(),
|
|
25110
|
+
/** The source's status; the copy itself is always OPEN. */
|
|
25111
|
+
sourceStatus: PentestFindingStatus,
|
|
25112
|
+
/** The new draft's id; null on a dry run. */
|
|
25113
|
+
pentestFindingId: external_exports.uuid().nullable(),
|
|
25114
|
+
title: external_exports.string(),
|
|
25115
|
+
severity: PentestFindingSeverity,
|
|
25116
|
+
evidenceCount: external_exports.int().nonnegative()
|
|
25117
|
+
});
|
|
25118
|
+
var CopyPentestFindingsResponseSchema = external_exports.object({
|
|
25119
|
+
dryRun: external_exports.boolean(),
|
|
25120
|
+
findings: external_exports.array(CopiedPentestFindingSchema)
|
|
25121
|
+
});
|
|
25089
25122
|
var RecoverStrandedFindingsRequestSchema = external_exports.object({
|
|
25090
25123
|
dryRun: external_exports.boolean().optional()
|
|
25091
25124
|
});
|
|
@@ -26119,6 +26152,9 @@ var CredentialPasskeySummarySchema = external_exports.object({
|
|
|
26119
26152
|
/** Moves only when the signature counter moved, the one observable sign of a use. */
|
|
26120
26153
|
lastAssertedAt: external_exports.coerce.date().nullable()
|
|
26121
26154
|
}).strict();
|
|
26155
|
+
var TestIntegrationPasskeySummarySchema = CredentialPasskeySummarySchema.omit({
|
|
26156
|
+
credentialPasskeyId: true
|
|
26157
|
+
}).extend({ testIntegrationPasskeyId: external_exports.uuid() }).strict();
|
|
26122
26158
|
var CredentialMfaKindValues = ["TOTP"];
|
|
26123
26159
|
var CredentialMfaKind = external_exports.enum(CredentialMfaKindValues);
|
|
26124
26160
|
var MFA_CAPABLE_CREDENTIAL_TYPES = ["USERNAME_PASSWORD", "MAGIC_AUTH"];
|
|
@@ -26528,24 +26564,40 @@ var RecordCandidateVulnResponseSchema = external_exports.object({
|
|
|
26528
26564
|
inserted: external_exports.boolean()
|
|
26529
26565
|
});
|
|
26530
26566
|
var ProductContextStatement = external_exports.string().min(1).max(8e3);
|
|
26567
|
+
var ProductEvidenceKindSchema = external_exports.enum(["docs", "source", "live"]);
|
|
26568
|
+
var EvidenceRefSchema = external_exports.object({
|
|
26569
|
+
ref: external_exports.string().min(1).max(512),
|
|
26570
|
+
why: external_exports.string().min(1).max(300)
|
|
26571
|
+
});
|
|
26572
|
+
var ProductFamilyEvidence = {
|
|
26573
|
+
evidence: ProductEvidenceKindSchema.optional(),
|
|
26574
|
+
refs: external_exports.array(EvidenceRefSchema).max(10).optional()
|
|
26575
|
+
};
|
|
26531
26576
|
var RecordProductContextRequestSchema = external_exports.object({
|
|
26532
26577
|
runVersion: external_exports.number().int().nonnegative(),
|
|
26533
26578
|
payload: external_exports.object({
|
|
26534
|
-
purpose: external_exports.object({ statement: ProductContextStatement }).optional(),
|
|
26535
|
-
authModel: external_exports.object({ statement: ProductContextStatement }).optional(),
|
|
26579
|
+
purpose: external_exports.object({ statement: ProductContextStatement, ...ProductFamilyEvidence }).optional(),
|
|
26580
|
+
authModel: external_exports.object({ statement: ProductContextStatement, ...ProductFamilyEvidence }).optional(),
|
|
26536
26581
|
techStack: external_exports.object({
|
|
26537
26582
|
statement: ProductContextStatement,
|
|
26538
26583
|
languages: external_exports.array(external_exports.string().min(1).max(80)).max(50).optional(),
|
|
26539
26584
|
frameworks: external_exports.array(external_exports.string().min(1).max(80)).max(50).optional(),
|
|
26540
|
-
services: external_exports.array(external_exports.string().min(1).max(80)).max(50).optional()
|
|
26585
|
+
services: external_exports.array(external_exports.string().min(1).max(80)).max(50).optional(),
|
|
26586
|
+
...ProductFamilyEvidence
|
|
26541
26587
|
}).optional(),
|
|
26542
26588
|
dataClassification: external_exports.object({
|
|
26543
26589
|
statement: ProductContextStatement,
|
|
26544
|
-
classes: external_exports.array(DataClassificationClass).min(1).max(40)
|
|
26590
|
+
classes: external_exports.array(DataClassificationClass).min(1).max(40),
|
|
26591
|
+
...ProductFamilyEvidence
|
|
26592
|
+
}).optional(),
|
|
26593
|
+
businessImpact: external_exports.object({
|
|
26594
|
+
statement: ProductContextStatement,
|
|
26595
|
+
level: BusinessImpactLevel,
|
|
26596
|
+
...ProductFamilyEvidence
|
|
26545
26597
|
}).optional(),
|
|
26546
|
-
|
|
26547
|
-
|
|
26548
|
-
architectureNotes: external_exports.array(external_exports.object({ statement: ProductContextStatement })).max(40).optional()
|
|
26598
|
+
deployment: external_exports.object({ statement: ProductContextStatement, ...ProductFamilyEvidence }).optional(),
|
|
26599
|
+
// The bare-array shape is kept for v1 payloads; each note carries its own evidence.
|
|
26600
|
+
architectureNotes: external_exports.array(external_exports.object({ statement: ProductContextStatement, ...ProductFamilyEvidence })).max(40).optional()
|
|
26549
26601
|
})
|
|
26550
26602
|
});
|
|
26551
26603
|
var RecordProductContextResponseSchema = external_exports.object({
|
|
@@ -27278,18 +27330,54 @@ var AgentSessionStoredPasskeySchema = AgentSessionPasskeySchema.extend({
|
|
|
27278
27330
|
privateKey: external_exports.string().min(1).max(4096),
|
|
27279
27331
|
userHandle: external_exports.string().max(128)
|
|
27280
27332
|
});
|
|
27281
|
-
var
|
|
27282
|
-
|
|
27283
|
-
|
|
27284
|
-
|
|
27285
|
-
|
|
27286
|
-
|
|
27287
|
-
|
|
27288
|
-
|
|
27289
|
-
|
|
27290
|
-
|
|
27291
|
-
|
|
27292
|
-
|
|
27333
|
+
var AgentSessionIntegrationPasskeySchema = AgentSessionPasskeySchema.extend({
|
|
27334
|
+
testIntegrationPasskeyId: external_exports.uuid(),
|
|
27335
|
+
privateKey: external_exports.string().min(1).max(4096),
|
|
27336
|
+
userHandle: external_exports.string().max(128)
|
|
27337
|
+
});
|
|
27338
|
+
var AgentSessionListPasskeysResponseSchema = external_exports.discriminatedUnion("owner", [
|
|
27339
|
+
external_exports.object({
|
|
27340
|
+
owner: external_exports.literal("credential"),
|
|
27341
|
+
credentialId: external_exports.uuid(),
|
|
27342
|
+
credentialLabel: external_exports.string().max(200),
|
|
27343
|
+
/** The account types a browser logs in as; the same set that can carry a second factor. */
|
|
27344
|
+
credentialType: external_exports.enum(MFA_CAPABLE_CREDENTIAL_TYPES),
|
|
27345
|
+
passkeys: external_exports.array(AgentSessionStoredPasskeySchema),
|
|
27346
|
+
/**
|
|
27347
|
+
* Stored rows whose envelope could not be read. Reported rather than swallowed so a
|
|
27348
|
+
* partial list is never presented to the agent as the whole store, and rather than
|
|
27349
|
+
* failing the call so the readable passkeys still load.
|
|
27350
|
+
*/
|
|
27351
|
+
unreadable: external_exports.number().int().min(0)
|
|
27352
|
+
}),
|
|
27353
|
+
external_exports.object({
|
|
27354
|
+
owner: external_exports.literal("testIntegration"),
|
|
27355
|
+
testIntegrationId: external_exports.uuid(),
|
|
27356
|
+
/** The integration's `label`, under the field name the host already keys on. */
|
|
27357
|
+
credentialLabel: external_exports.string().max(200),
|
|
27358
|
+
passkeys: external_exports.array(AgentSessionIntegrationPasskeySchema),
|
|
27359
|
+
unreadable: external_exports.number().int().min(0)
|
|
27360
|
+
})
|
|
27361
|
+
]);
|
|
27362
|
+
var AgentSessionTestIntegrationPasskeyUseSchema = AgentSessionPasskeySchema.pick({
|
|
27363
|
+
rpId: true,
|
|
27364
|
+
webauthnCredentialId: true,
|
|
27365
|
+
signCount: true,
|
|
27366
|
+
backupEligibility: true,
|
|
27367
|
+
backupState: true
|
|
27368
|
+
}).strict();
|
|
27369
|
+
var AgentSessionRecordTestIntegrationPasskeyUseRequestSchema = external_exports.object({
|
|
27370
|
+
runVersion: external_exports.number().int().nonnegative(),
|
|
27371
|
+
/** From the list response, never agent-supplied. */
|
|
27372
|
+
testIntegrationId: external_exports.uuid(),
|
|
27373
|
+
passkey: AgentSessionTestIntegrationPasskeyUseSchema
|
|
27374
|
+
});
|
|
27375
|
+
var AgentSessionRecordTestIntegrationPasskeyUseResponseSchema = external_exports.object({
|
|
27376
|
+
testIntegrationPasskeyId: external_exports.uuid(),
|
|
27377
|
+
/** The stored counter after the call, never lower than before. */
|
|
27378
|
+
signCount: external_exports.number().int().min(0),
|
|
27379
|
+
/** False when the stored counter was already at or past the reported one. */
|
|
27380
|
+
counterMoved: external_exports.boolean()
|
|
27293
27381
|
});
|
|
27294
27382
|
var AgentSessionUpsertPasskeyRequestSchema = external_exports.object({
|
|
27295
27383
|
runVersion: external_exports.number().int().nonnegative(),
|
|
@@ -28304,6 +28392,27 @@ var agentSessionInternalContract = c4.router({
|
|
|
28304
28392
|
500: ErrorResponseSchema
|
|
28305
28393
|
}
|
|
28306
28394
|
},
|
|
28395
|
+
// provision_passkey, integration write half: a stored test-integration passkey's
|
|
28396
|
+
// counter moved on the armed authenticator. Counter-only by shape (no key field on
|
|
28397
|
+
// the body); idempotent on (testIntegrationId, rpId, webauthnCredentialId).
|
|
28398
|
+
recordTestIntegrationPasskeyUse: {
|
|
28399
|
+
method: "POST",
|
|
28400
|
+
path: "/internal/v1/agent-sessions/:agentSessionId/test-integration-passkeys/use",
|
|
28401
|
+
pathParams: AgentSessionInternalParamsSchema,
|
|
28402
|
+
body: AgentSessionRecordTestIntegrationPasskeyUseRequestSchema,
|
|
28403
|
+
responses: {
|
|
28404
|
+
200: AgentSessionRecordTestIntegrationPasskeyUseResponseSchema,
|
|
28405
|
+
400: ErrorResponseSchema,
|
|
28406
|
+
401: ErrorResponseSchema,
|
|
28407
|
+
// Session type receives no account credentials, or may not fetch test integrations.
|
|
28408
|
+
403: ErrorResponseSchema,
|
|
28409
|
+
// No stored passkey with that natural key on that integration (staff deleted it).
|
|
28410
|
+
404: ErrorResponseSchema,
|
|
28411
|
+
// RUN_REVOKED.
|
|
28412
|
+
409: ErrorResponseSchema,
|
|
28413
|
+
500: ErrorResponseSchema
|
|
28414
|
+
}
|
|
28415
|
+
},
|
|
28307
28416
|
// renew_cloud_credentials (GCP integration PR 7): agent-invoked re-mint of the
|
|
28308
28417
|
// ~1 h AWS/GCP recon creds. Run-token authed; org+pentest from the session row;
|
|
28309
28418
|
// 403 for a non-credentialed session type (ADHOC/CODE_REVIEW/REPORT).
|
|
@@ -33673,6 +33782,14 @@ var CreateOrganizationRequestSchema = external_exports.object({
|
|
|
33673
33782
|
homepageUrl: external_exports.url("Homepage must be a full URL, like https://acme.com").max(2048).nullable().optional()
|
|
33674
33783
|
});
|
|
33675
33784
|
var LogoFetchOutcomeValues = ["skipped", "stored", "not_found", "failed"];
|
|
33785
|
+
var RenameOrganizationRequestSchema = external_exports.object({
|
|
33786
|
+
name: external_exports.string().trim().min(2, "Organization name must be at least 2 characters").max(100, "Organization name must be 100 characters or fewer")
|
|
33787
|
+
}).strict();
|
|
33788
|
+
var RenameOrganizationResponseSchema = external_exports.object({
|
|
33789
|
+
organizationId: external_exports.uuid(),
|
|
33790
|
+
slug: external_exports.string(),
|
|
33791
|
+
name: external_exports.string()
|
|
33792
|
+
});
|
|
33676
33793
|
var CreateOrganizationResponseSchema = external_exports.object({
|
|
33677
33794
|
organizationId: external_exports.uuid(),
|
|
33678
33795
|
slug: external_exports.string(),
|
|
@@ -33760,6 +33877,23 @@ var organizationContract = c27.router({
|
|
|
33760
33877
|
},
|
|
33761
33878
|
summary: "Create an organization (Trace staff only)"
|
|
33762
33879
|
},
|
|
33880
|
+
// Staff-only, cross-org: renames the tenant in Trace and WorkOS together.
|
|
33881
|
+
// The slug stays as it was.
|
|
33882
|
+
renameOrganizationAsStaff: {
|
|
33883
|
+
method: "PATCH",
|
|
33884
|
+
path: "/staff/organizations/:organizationId",
|
|
33885
|
+
pathParams: OrgIdParamsSchema,
|
|
33886
|
+
body: RenameOrganizationRequestSchema,
|
|
33887
|
+
responses: {
|
|
33888
|
+
200: RenameOrganizationResponseSchema,
|
|
33889
|
+
400: ErrorResponseSchema,
|
|
33890
|
+
401: ErrorResponseSchema,
|
|
33891
|
+
403: ErrorResponseSchema,
|
|
33892
|
+
404: ErrorResponseSchema,
|
|
33893
|
+
500: ErrorResponseSchema
|
|
33894
|
+
},
|
|
33895
|
+
summary: "Rename an organization (Trace staff only)"
|
|
33896
|
+
},
|
|
33763
33897
|
// Staff-only, cross-org: removes the tenant from Trace and WorkOS together.
|
|
33764
33898
|
// 409 when members, repositories, or retained audit history still reference
|
|
33765
33899
|
// the organization; those must be removed first (or cannot be, for WORM rows).
|
|
@@ -35756,6 +35890,23 @@ var pentestFindingContract = c37.router({
|
|
|
35756
35890
|
500: ErrorResponseSchema
|
|
35757
35891
|
}
|
|
35758
35892
|
},
|
|
35893
|
+
// Staff-only. `:pentestId` is the target; the source is in the body.
|
|
35894
|
+
copyFindings: {
|
|
35895
|
+
method: "POST",
|
|
35896
|
+
path: "/organizations/:organizationId/pentests/:pentestId/findings/copies",
|
|
35897
|
+
pathParams: PentestFindingParamsSchema,
|
|
35898
|
+
body: CopyPentestFindingsRequestSchema,
|
|
35899
|
+
responses: {
|
|
35900
|
+
200: CopyPentestFindingsResponseSchema,
|
|
35901
|
+
201: CopyPentestFindingsResponseSchema,
|
|
35902
|
+
400: ErrorResponseSchema,
|
|
35903
|
+
401: ErrorResponseSchema,
|
|
35904
|
+
403: ErrorResponseSchema,
|
|
35905
|
+
404: ErrorResponseSchema,
|
|
35906
|
+
409: ErrorResponseSchema,
|
|
35907
|
+
500: ErrorResponseSchema
|
|
35908
|
+
}
|
|
35909
|
+
},
|
|
35759
35910
|
recoverStrandedFindings: {
|
|
35760
35911
|
method: "POST",
|
|
35761
35912
|
path: "/organizations/:organizationId/pentests/:pentestId/findings/recover-stranded",
|
|
@@ -37449,6 +37600,9 @@ var SetStaffOrganizationPentestCustomerVisibilityRequestSchema = external_export
|
|
|
37449
37600
|
/** false hides the engagement from the customer entirely; true shows it again. */
|
|
37450
37601
|
visible: external_exports.boolean()
|
|
37451
37602
|
}).strict();
|
|
37603
|
+
var RenameStaffOrganizationPentestRequestSchema = external_exports.object({
|
|
37604
|
+
title: external_exports.string().trim().min(1, "Title is required").max(500, "Title must be 500 characters or fewer")
|
|
37605
|
+
}).strict();
|
|
37452
37606
|
|
|
37453
37607
|
// ../../packages/api-contracts/src/contracts/v1/staff-pentests.ts
|
|
37454
37608
|
var c47 = initContract();
|
|
@@ -37479,6 +37633,22 @@ var staffPentestsContract = c47.router({
|
|
|
37479
37633
|
},
|
|
37480
37634
|
summary: "Soft-delete a pentest from the admin console (Trace staff only)"
|
|
37481
37635
|
},
|
|
37636
|
+
renameStaffOrganizationPentest: {
|
|
37637
|
+
method: "PATCH",
|
|
37638
|
+
path: "/organizations/:organizationId/staff/pentests/:pentestId",
|
|
37639
|
+
pathParams: StaffOrganizationPentestParamsSchema,
|
|
37640
|
+
body: RenameStaffOrganizationPentestRequestSchema,
|
|
37641
|
+
responses: {
|
|
37642
|
+
200: StaffOrganizationPentestSummarySchema,
|
|
37643
|
+
400: ErrorResponseSchema,
|
|
37644
|
+
401: ErrorResponseSchema,
|
|
37645
|
+
403: ErrorResponseSchema,
|
|
37646
|
+
404: ErrorResponseSchema,
|
|
37647
|
+
409: ErrorResponseSchema,
|
|
37648
|
+
500: ErrorResponseSchema
|
|
37649
|
+
},
|
|
37650
|
+
summary: "Rename a pentest from the admin console (Trace staff only)"
|
|
37651
|
+
},
|
|
37482
37652
|
setStaffOrganizationPentestCustomerVisibility: {
|
|
37483
37653
|
method: "POST",
|
|
37484
37654
|
path: "/organizations/:organizationId/staff/pentests/:pentestId/customer-visibility",
|
|
@@ -37623,6 +37793,9 @@ var NOTES_MAX = 2e3;
|
|
|
37623
37793
|
var TestIntegrationIdParamsSchema = external_exports.object({
|
|
37624
37794
|
testIntegrationId: external_exports.uuid()
|
|
37625
37795
|
});
|
|
37796
|
+
var TestIntegrationPasskeyIdParamsSchema = TestIntegrationIdParamsSchema.extend({
|
|
37797
|
+
testIntegrationPasskeyId: external_exports.uuid()
|
|
37798
|
+
});
|
|
37626
37799
|
var TestIntegrationResponseSchema = external_exports.object({
|
|
37627
37800
|
testIntegrationId: external_exports.uuid(),
|
|
37628
37801
|
label: external_exports.string(),
|
|
@@ -37635,9 +37808,15 @@ var TestIntegrationResponseSchema = external_exports.object({
|
|
|
37635
37808
|
* none (or its stored parameters are unreadable). The setup key is never
|
|
37636
37809
|
* returned by any route.
|
|
37637
37810
|
*/
|
|
37638
|
-
mfa: CredentialMfaSummarySchema.nullable()
|
|
37811
|
+
mfa: CredentialMfaSummarySchema.nullable(),
|
|
37812
|
+
/** Passkeys staff stored for the integration's account. A count only: nothing here is decrypted. */
|
|
37813
|
+
passkeyCount: external_exports.number().int().min(0)
|
|
37639
37814
|
}).strict();
|
|
37640
37815
|
var ListTestIntegrationsResponseSchema = external_exports.array(TestIntegrationResponseSchema);
|
|
37816
|
+
var TestIntegrationPasskeyImportSchema = AgentSessionPasskeySchema.required({
|
|
37817
|
+
privateKey: true,
|
|
37818
|
+
userHandle: true
|
|
37819
|
+
}).strict();
|
|
37641
37820
|
var TestIntegrationLoginSchema = external_exports.object({
|
|
37642
37821
|
username: external_exports.string().min(1).max(512),
|
|
37643
37822
|
password: external_exports.string().min(1).max(1024)
|
|
@@ -37762,6 +37941,54 @@ var testIntegrationContract = c49.router({
|
|
|
37762
37941
|
404: ErrorResponseSchema,
|
|
37763
37942
|
500: ErrorResponseSchema
|
|
37764
37943
|
}
|
|
37944
|
+
},
|
|
37945
|
+
// -- Passkeys (org-less, verified staff) -----------------------------------
|
|
37946
|
+
// Staff enroll a passkey on the integration's account with the capture script
|
|
37947
|
+
// and import the captured file here. Agents never reach these routes; the agent
|
|
37948
|
+
// read path is provision_passkey on the run-token channel. A stored key signs in
|
|
37949
|
+
// as a Trace-owned account, so every route takes the MFA routes' verified-staff
|
|
37950
|
+
// bar. Import is never an upsert: a duplicate is a mistake to surface.
|
|
37951
|
+
importTestIntegrationPasskey: {
|
|
37952
|
+
method: "POST",
|
|
37953
|
+
path: "/staff/test-integrations/:testIntegrationId/passkeys",
|
|
37954
|
+
pathParams: TestIntegrationIdParamsSchema,
|
|
37955
|
+
body: TestIntegrationPasskeyImportSchema,
|
|
37956
|
+
responses: {
|
|
37957
|
+
201: TestIntegrationPasskeySummarySchema,
|
|
37958
|
+
400: ErrorResponseSchema,
|
|
37959
|
+
401: ErrorResponseSchema,
|
|
37960
|
+
403: ErrorResponseSchema,
|
|
37961
|
+
404: ErrorResponseSchema,
|
|
37962
|
+
409: ErrorResponseSchema,
|
|
37963
|
+
// TEST_INTEGRATION_PASSKEY_EXISTS
|
|
37964
|
+
500: ErrorResponseSchema
|
|
37965
|
+
}
|
|
37966
|
+
},
|
|
37967
|
+
listTestIntegrationPasskeys: {
|
|
37968
|
+
method: "GET",
|
|
37969
|
+
path: "/staff/test-integrations/:testIntegrationId/passkeys",
|
|
37970
|
+
pathParams: TestIntegrationIdParamsSchema,
|
|
37971
|
+
responses: {
|
|
37972
|
+
200: external_exports.array(TestIntegrationPasskeySummarySchema),
|
|
37973
|
+
401: ErrorResponseSchema,
|
|
37974
|
+
403: ErrorResponseSchema,
|
|
37975
|
+
404: ErrorResponseSchema,
|
|
37976
|
+
500: ErrorResponseSchema
|
|
37977
|
+
}
|
|
37978
|
+
},
|
|
37979
|
+
// Not idempotent: the id names a specific row, and a second delete should say it
|
|
37980
|
+
// is gone. The passkey on the site's side stays.
|
|
37981
|
+
deleteTestIntegrationPasskey: {
|
|
37982
|
+
method: "DELETE",
|
|
37983
|
+
path: "/staff/test-integrations/:testIntegrationId/passkeys/:testIntegrationPasskeyId",
|
|
37984
|
+
pathParams: TestIntegrationPasskeyIdParamsSchema,
|
|
37985
|
+
responses: {
|
|
37986
|
+
204: DeleteResponseSchema,
|
|
37987
|
+
401: ErrorResponseSchema,
|
|
37988
|
+
403: ErrorResponseSchema,
|
|
37989
|
+
404: ErrorResponseSchema,
|
|
37990
|
+
500: ErrorResponseSchema
|
|
37991
|
+
}
|
|
37765
37992
|
}
|
|
37766
37993
|
});
|
|
37767
37994
|
|
|
@@ -41908,22 +42135,30 @@ function toIso(value) {
|
|
|
41908
42135
|
return Number.isNaN(date5.getTime()) ? String(value) : date5.toISOString();
|
|
41909
42136
|
}
|
|
41910
42137
|
async function confirmDeletion(description, skip) {
|
|
41911
|
-
|
|
42138
|
+
return confirmAction(`Delete ${description}?`, { skip, verb: "delete" });
|
|
42139
|
+
}
|
|
42140
|
+
async function confirmAction(question, options) {
|
|
42141
|
+
if (options.skip) {
|
|
41912
42142
|
return true;
|
|
41913
42143
|
}
|
|
41914
42144
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
41915
|
-
console.error(
|
|
42145
|
+
console.error(
|
|
42146
|
+
colorError(`Refusing to ${options.verb} without --yes in a non-interactive shell.`)
|
|
42147
|
+
);
|
|
41916
42148
|
process.exit(1);
|
|
41917
42149
|
}
|
|
41918
42150
|
const readline = createInterface({ input: process.stdin, output: process.stdout });
|
|
41919
42151
|
try {
|
|
41920
|
-
const answer = await readline.question(
|
|
42152
|
+
const answer = await readline.question(`${question} [y/N] `);
|
|
41921
42153
|
const normalized = answer.trim().toLowerCase();
|
|
41922
42154
|
return normalized === "y" || normalized === "yes";
|
|
41923
42155
|
} finally {
|
|
41924
42156
|
readline.close();
|
|
41925
42157
|
}
|
|
41926
42158
|
}
|
|
42159
|
+
function collectRepeatable(value, previous) {
|
|
42160
|
+
return [...previous ?? [], value];
|
|
42161
|
+
}
|
|
41927
42162
|
|
|
41928
42163
|
// src/application/commands.ts
|
|
41929
42164
|
var APPLICATION_TYPE_BY_INPUT = {
|
|
@@ -47544,11 +47779,46 @@ var CLI_VERSION = packageMetadata.version;
|
|
|
47544
47779
|
// src/pentest/commands.ts
|
|
47545
47780
|
import { readFileSync as readFileSync7 } from "fs";
|
|
47546
47781
|
|
|
47547
|
-
// src/pentest/
|
|
47782
|
+
// src/pentest/evidence.ts
|
|
47548
47783
|
import { readFileSync as readFileSync5, statSync as statSync2 } from "fs";
|
|
47784
|
+
import { basename as basename2 } from "path";
|
|
47549
47785
|
|
|
47550
47786
|
// src/pentest/resolve.ts
|
|
47551
47787
|
var UUID_PATTERN2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
47788
|
+
async function resolveApplicationId(organizationId, identifier) {
|
|
47789
|
+
if (UUID_PATTERN2.test(identifier)) {
|
|
47790
|
+
return identifier;
|
|
47791
|
+
}
|
|
47792
|
+
const result = await authenticatedRequest(
|
|
47793
|
+
(client) => client.application.listApplications({ params: { organizationId } })
|
|
47794
|
+
);
|
|
47795
|
+
if (result.status === UNAUTHORIZED_STATUS2) {
|
|
47796
|
+
exitAuthFailed();
|
|
47797
|
+
}
|
|
47798
|
+
if (result.status !== 200) {
|
|
47799
|
+
console.error(colorError(`Failed to list applications (status ${result.status}).`));
|
|
47800
|
+
process.exit(1);
|
|
47801
|
+
}
|
|
47802
|
+
const wanted = identifier.trim().toLowerCase();
|
|
47803
|
+
const matches = result.body.filter((app) => app.name.trim().toLowerCase() === wanted);
|
|
47804
|
+
if (matches.length === 1 && matches[0]) {
|
|
47805
|
+
return matches[0].applicationId;
|
|
47806
|
+
}
|
|
47807
|
+
if (matches.length > 1) {
|
|
47808
|
+
console.error(
|
|
47809
|
+
colorError(
|
|
47810
|
+
`Application name '${sanitizeDisplay(identifier)}' is ambiguous. Pass the application ID instead (\`tracecli app list\`).`
|
|
47811
|
+
)
|
|
47812
|
+
);
|
|
47813
|
+
process.exit(1);
|
|
47814
|
+
}
|
|
47815
|
+
console.error(
|
|
47816
|
+
colorError(
|
|
47817
|
+
`Application '${sanitizeDisplay(identifier)}' not found. Run \`tracecli app list\` to see available applications.`
|
|
47818
|
+
)
|
|
47819
|
+
);
|
|
47820
|
+
process.exit(1);
|
|
47821
|
+
}
|
|
47552
47822
|
async function resolvePentestId(organizationId, identifier) {
|
|
47553
47823
|
if (UUID_PATTERN2.test(identifier)) {
|
|
47554
47824
|
return identifier;
|
|
@@ -47585,61 +47855,360 @@ Run \`tracecli pentest list\` to see available pentests.`
|
|
|
47585
47855
|
return pentest.pentestId;
|
|
47586
47856
|
}
|
|
47587
47857
|
|
|
47588
|
-
// src/pentest/
|
|
47589
|
-
var
|
|
47590
|
-
|
|
47591
|
-
|
|
47858
|
+
// src/pentest/evidence.ts
|
|
47859
|
+
var SECTION_LIST = ValidationEvidenceSectionValues.join(", ");
|
|
47860
|
+
var KIND_LIST = PentestExploitArtifactKindValues.join(", ");
|
|
47861
|
+
function isSection(value) {
|
|
47862
|
+
return ValidationEvidenceSectionValues.includes(value);
|
|
47592
47863
|
}
|
|
47593
|
-
function
|
|
47594
|
-
|
|
47595
|
-
if (parsed.success) {
|
|
47596
|
-
return parsed.data;
|
|
47597
|
-
}
|
|
47598
|
-
return {
|
|
47599
|
-
error: `Invalid --severity '${sanitizeDisplay(raw)}'. Use one of: ${SEVERITY_LIST}.`
|
|
47600
|
-
};
|
|
47864
|
+
function isKind(value) {
|
|
47865
|
+
return PentestExploitArtifactKindValues.includes(value);
|
|
47601
47866
|
}
|
|
47602
|
-
function
|
|
47603
|
-
|
|
47604
|
-
if (
|
|
47605
|
-
|
|
47606
|
-
error: `Invalid --cvss-score '${sanitizeDisplay(raw)}'. Expected a number between 0 and 10.`
|
|
47607
|
-
};
|
|
47608
|
-
}
|
|
47609
|
-
return score;
|
|
47867
|
+
function formatBytes2(bytes) {
|
|
47868
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
47869
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
|
|
47870
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
47610
47871
|
}
|
|
47611
|
-
function
|
|
47612
|
-
|
|
47613
|
-
|
|
47614
|
-
parsed = JSON.parse(raw);
|
|
47615
|
-
} catch {
|
|
47616
|
-
return { error: `Invalid JSON in ${sourceLabel}.` };
|
|
47872
|
+
async function resolveFindingId(organizationId, pentestId, identifier) {
|
|
47873
|
+
if (isUuid(identifier)) {
|
|
47874
|
+
return identifier;
|
|
47617
47875
|
}
|
|
47618
|
-
|
|
47619
|
-
|
|
47620
|
-
|
|
47621
|
-
|
|
47876
|
+
const numericMatch = identifier.match(/^(?:PTF-)?(\d+)$/i);
|
|
47877
|
+
if (!numericMatch?.[1]) {
|
|
47878
|
+
console.error(
|
|
47879
|
+
colorError(
|
|
47880
|
+
`Invalid finding identifier '${sanitizeDisplay(identifier)}'. Use a finding number (e.g. PTF-7) or UUID.`
|
|
47881
|
+
)
|
|
47882
|
+
);
|
|
47883
|
+
process.exit(1);
|
|
47622
47884
|
}
|
|
47623
|
-
|
|
47885
|
+
const displayId = Number.parseInt(numericMatch[1], 10);
|
|
47886
|
+
const result = await authenticatedRequest(
|
|
47887
|
+
(client) => client.pentestFinding.listFindings({ params: { organizationId, pentestId } })
|
|
47888
|
+
);
|
|
47889
|
+
if (result.status === UNAUTHORIZED_STATUS2) {
|
|
47890
|
+
exitAuthFailed();
|
|
47891
|
+
}
|
|
47892
|
+
if (result.status !== 200) {
|
|
47893
|
+
console.error(colorError(`Failed to list findings (status ${result.status}).`));
|
|
47894
|
+
process.exit(1);
|
|
47895
|
+
}
|
|
47896
|
+
const match = result.body.find((f) => f.displayId === displayId);
|
|
47897
|
+
if (!match || !("pentestFindingId" in match)) {
|
|
47898
|
+
console.error(
|
|
47899
|
+
colorError(
|
|
47900
|
+
`No finding PTF-${displayId} in this pentest. Unpublished findings have no number yet \u2014 pass the finding UUID instead (\`tracecli pentest findings list --pentest <id> --output json\`).`
|
|
47901
|
+
)
|
|
47902
|
+
);
|
|
47903
|
+
process.exit(1);
|
|
47904
|
+
}
|
|
47905
|
+
return match.pentestFindingId;
|
|
47624
47906
|
}
|
|
47625
|
-
function
|
|
47907
|
+
async function runAttachEvidence(options) {
|
|
47908
|
+
if (options.section !== void 0 && !isSection(options.section)) {
|
|
47909
|
+
console.error(colorError(`Invalid --section '${sanitizeDisplay(options.section)}'.`));
|
|
47910
|
+
console.error(`Valid sections: ${SECTION_LIST}`);
|
|
47911
|
+
process.exit(1);
|
|
47912
|
+
}
|
|
47913
|
+
if (options.kind !== void 0 && !isKind(options.kind)) {
|
|
47914
|
+
console.error(colorError(`Invalid --kind '${sanitizeDisplay(options.kind)}'.`));
|
|
47915
|
+
console.error(`Valid kinds: ${KIND_LIST}`);
|
|
47916
|
+
process.exit(1);
|
|
47917
|
+
}
|
|
47918
|
+
let sizeBytes;
|
|
47626
47919
|
try {
|
|
47627
|
-
const stats = statSync2(
|
|
47920
|
+
const stats = statSync2(options.file);
|
|
47628
47921
|
if (!stats.isFile()) {
|
|
47629
|
-
|
|
47922
|
+
console.error(colorError(`--file '${sanitizeDisplay(options.file)}' is not a regular file.`));
|
|
47923
|
+
process.exit(1);
|
|
47630
47924
|
}
|
|
47925
|
+
sizeBytes = stats.size;
|
|
47631
47926
|
} catch {
|
|
47632
|
-
|
|
47927
|
+
console.error(colorError(`Cannot read --file '${sanitizeDisplay(options.file)}'.`));
|
|
47928
|
+
process.exit(1);
|
|
47633
47929
|
}
|
|
47634
|
-
|
|
47635
|
-
|
|
47636
|
-
|
|
47930
|
+
if (sizeBytes === 0) {
|
|
47931
|
+
console.error(colorError(`--file '${sanitizeDisplay(options.file)}' is empty.`));
|
|
47932
|
+
process.exit(1);
|
|
47637
47933
|
}
|
|
47638
|
-
|
|
47639
|
-
|
|
47640
|
-
|
|
47641
|
-
|
|
47642
|
-
|
|
47934
|
+
if (sizeBytes > MAX_STAFF_EVIDENCE_UPLOAD_BYTES) {
|
|
47935
|
+
console.error(
|
|
47936
|
+
colorError(
|
|
47937
|
+
`File is ${formatBytes2(sizeBytes)}; the limit for one exhibit is ${formatBytes2(MAX_STAFF_EVIDENCE_UPLOAD_BYTES)}.`
|
|
47938
|
+
)
|
|
47939
|
+
);
|
|
47940
|
+
process.exit(1);
|
|
47941
|
+
}
|
|
47942
|
+
const organizationId = await resolveOrganizationId();
|
|
47943
|
+
const pentestId = await resolvePentestId(organizationId, options.pentest);
|
|
47944
|
+
const pentestFindingId = await resolveFindingId(organizationId, pentestId, options.finding);
|
|
47945
|
+
const params = { organizationId, pentestId, pentestFindingId };
|
|
47946
|
+
const filename = basename2(options.file);
|
|
47947
|
+
const slot = await authenticatedRequest(
|
|
47948
|
+
(client) => client.pentestFinding.createFindingEvidenceUpload({
|
|
47949
|
+
params,
|
|
47950
|
+
body: { filename, sizeBytes }
|
|
47951
|
+
})
|
|
47952
|
+
);
|
|
47953
|
+
if (slot.status === UNAUTHORIZED_STATUS2) {
|
|
47954
|
+
exitAuthFailed();
|
|
47955
|
+
}
|
|
47956
|
+
if (slot.status !== 201) {
|
|
47957
|
+
const { message } = readErrorFields(slot.body);
|
|
47958
|
+
console.error(
|
|
47959
|
+
colorError(message ?? `Failed to start the evidence upload (status ${slot.status}).`)
|
|
47960
|
+
);
|
|
47961
|
+
process.exit(1);
|
|
47962
|
+
}
|
|
47963
|
+
const body = readFileSync5(options.file);
|
|
47964
|
+
const put = await fetch(slot.body.uploadUrl, { method: "PUT", body: new Uint8Array(body) });
|
|
47965
|
+
if (!put.ok) {
|
|
47966
|
+
console.error(
|
|
47967
|
+
colorError(
|
|
47968
|
+
`Upload to storage failed (status ${put.status}). The upload link is short-lived \u2014 re-run the command.`
|
|
47969
|
+
)
|
|
47970
|
+
);
|
|
47971
|
+
process.exit(1);
|
|
47972
|
+
}
|
|
47973
|
+
const attached = await authenticatedRequest(
|
|
47974
|
+
(client) => client.pentestFinding.attachFindingEvidence({
|
|
47975
|
+
params,
|
|
47976
|
+
body: {
|
|
47977
|
+
key: slot.body.key,
|
|
47978
|
+
...options.caption !== void 0 ? { caption: options.caption } : {},
|
|
47979
|
+
...options.section !== void 0 && isSection(options.section) ? { section: options.section } : {},
|
|
47980
|
+
...options.kind !== void 0 && isKind(options.kind) ? { kind: options.kind } : {},
|
|
47981
|
+
...options.customerFacing ? { customerFacing: true } : {}
|
|
47982
|
+
}
|
|
47983
|
+
})
|
|
47984
|
+
);
|
|
47985
|
+
if (attached.status === UNAUTHORIZED_STATUS2) {
|
|
47986
|
+
exitAuthFailed();
|
|
47987
|
+
}
|
|
47988
|
+
if (attached.status !== 201) {
|
|
47989
|
+
const { message } = readErrorFields(attached.body);
|
|
47990
|
+
console.error(
|
|
47991
|
+
colorError(message ?? `Failed to attach the evidence (status ${attached.status}).`)
|
|
47992
|
+
);
|
|
47993
|
+
process.exit(1);
|
|
47994
|
+
}
|
|
47995
|
+
if (options.output === "json") {
|
|
47996
|
+
printJson(attached.body);
|
|
47997
|
+
return;
|
|
47998
|
+
}
|
|
47999
|
+
const visibility = attached.body.customerFacing ? "customer-facing" : "staff-internal";
|
|
48000
|
+
const placement = attached.body.section ? ` \xB7 ${attached.body.section}` : "";
|
|
48001
|
+
console.log(
|
|
48002
|
+
colorSuccess(
|
|
48003
|
+
`Attached ${attached.body.kind} \xB7 ${formatBytes2(attached.body.sizeBytes)} \xB7 ${visibility}${placement}`
|
|
48004
|
+
)
|
|
48005
|
+
);
|
|
48006
|
+
console.log(` artifact ${attached.body.artifactId}`);
|
|
48007
|
+
if (attached.body.kind === "HAR") {
|
|
48008
|
+
console.log(" HAR captures stay internal \u2014 they can carry a live session token.");
|
|
48009
|
+
} else if (!attached.body.customerFacing) {
|
|
48010
|
+
console.log(" Publish it from the finding\u2019s evidence panel in the staff console.");
|
|
48011
|
+
}
|
|
48012
|
+
}
|
|
48013
|
+
function createAttachEvidenceCommand() {
|
|
48014
|
+
return new Command("attach-evidence").description("Attach a screenshot, recording, or capture to a pentest finding (staff)").requiredOption("--pentest <id>", "Pentest display ID (e.g. PT-3) or UUID").requiredOption("--finding <id>", "Finding number (e.g. PTF-7) or UUID").requiredOption("--file <path>", "Path to the evidence file").option("--caption <text>", "What the exhibit shows; rendered as its label").option("--section <section>", `Report section it backs (${SECTION_LIST})`).option("--kind <kind>", `Override the derived exhibit kind (${KIND_LIST})`).option("--customer-facing", "Publish to the customer (not allowed for HAR or video)").option("-o, --output <format>", "Output format: text, json (default: text)", "text").addHelpText(
|
|
48015
|
+
"after",
|
|
48016
|
+
`
|
|
48017
|
+
The file uploads straight to storage, so recordings of any reasonable size work.
|
|
48018
|
+
Videos and HAR captures always land staff-internal \u2014 publish a video with the
|
|
48019
|
+
toggle in the staff console's evidence panel after reviewing it for redaction.
|
|
48020
|
+
|
|
48021
|
+
Examples:
|
|
48022
|
+
tracecli pentest findings attach-evidence --pentest PT-3 --finding 7 --file ./poc.png
|
|
48023
|
+
tracecli pentest findings attach-evidence --pentest PT-3 --finding PTF-7 --file ./poc.mov \\
|
|
48024
|
+
--caption "Cookie survives logout, replayed 40s later" --section proofOfConcept
|
|
48025
|
+
tracecli pentest findings attach-evidence --pentest PT-3 --finding 7 --file ./roster.json \\
|
|
48026
|
+
--caption "Full member roster returned to a non-member" --customer-facing --output json`
|
|
48027
|
+
).action(async (options) => {
|
|
48028
|
+
await runAttachEvidence(options);
|
|
48029
|
+
});
|
|
48030
|
+
}
|
|
48031
|
+
|
|
48032
|
+
// src/pentest/copy-findings.ts
|
|
48033
|
+
function validateSelection(options) {
|
|
48034
|
+
const hasApplication = options.application !== void 0;
|
|
48035
|
+
const hasFindings = options.finding !== void 0 && options.finding.length > 0;
|
|
48036
|
+
if (hasApplication === hasFindings) {
|
|
48037
|
+
return {
|
|
48038
|
+
error: "Pass exactly one of --application <id|name> or --finding <id> (repeatable)."
|
|
48039
|
+
};
|
|
48040
|
+
}
|
|
48041
|
+
return null;
|
|
48042
|
+
}
|
|
48043
|
+
function failCopy(status, body) {
|
|
48044
|
+
const { message, errorCode } = readErrorFields(body);
|
|
48045
|
+
if (status === UNAUTHORIZED_STATUS2) {
|
|
48046
|
+
exitAuthFailed();
|
|
48047
|
+
}
|
|
48048
|
+
if (status === 403) {
|
|
48049
|
+
console.error(
|
|
48050
|
+
colorError(
|
|
48051
|
+
"Only Trace staff can copy pentest findings. Sign in with a staff account via `tracecli auth login`."
|
|
48052
|
+
)
|
|
48053
|
+
);
|
|
48054
|
+
process.exit(1);
|
|
48055
|
+
}
|
|
48056
|
+
if (status === 404) {
|
|
48057
|
+
console.error(colorError(message ?? "Pentest, application, or finding not found."));
|
|
48058
|
+
process.exit(1);
|
|
48059
|
+
}
|
|
48060
|
+
if (status === 400) {
|
|
48061
|
+
console.error(colorError(message ?? "Invalid request (status 400)."));
|
|
48062
|
+
process.exit(1);
|
|
48063
|
+
}
|
|
48064
|
+
if (status === 409) {
|
|
48065
|
+
console.error(
|
|
48066
|
+
colorError(
|
|
48067
|
+
errorCode === "PENTEST_CLOSED" ? message ?? "Closed pentests must be reopened before copying findings into them." : message ?? "Conflict (status 409)."
|
|
48068
|
+
)
|
|
48069
|
+
);
|
|
48070
|
+
process.exit(1);
|
|
48071
|
+
}
|
|
48072
|
+
console.error(
|
|
48073
|
+
colorError(`Failed to copy findings (status ${status})${message ? `: ${message}` : "."}`)
|
|
48074
|
+
);
|
|
48075
|
+
process.exit(1);
|
|
48076
|
+
}
|
|
48077
|
+
function formatCopiedFinding(finding) {
|
|
48078
|
+
const source = finding.sourceDisplayId === null ? "draft" : `PTF-${finding.sourceDisplayId}`;
|
|
48079
|
+
const status = sanitizeDisplay(finding.sourceStatus).toLowerCase().replace(/_/g, " ");
|
|
48080
|
+
const evidence = `${finding.evidenceCount} exhibit${finding.evidenceCount === 1 ? "" : "s"}`;
|
|
48081
|
+
const target = finding.pentestFindingId === null ? "" : ` -> ${finding.pentestFindingId}`;
|
|
48082
|
+
return ` [${sanitizeDisplay(finding.severity)}] ${sanitizeDisplay(finding.title)} (${source}, ${status}, ${evidence})${target}`;
|
|
48083
|
+
}
|
|
48084
|
+
async function copyFindings(options) {
|
|
48085
|
+
const selection = validateSelection(options);
|
|
48086
|
+
if (selection) {
|
|
48087
|
+
console.error(colorError(selection.error));
|
|
48088
|
+
process.exit(1);
|
|
48089
|
+
}
|
|
48090
|
+
const organizationId = await resolveOrganizationId();
|
|
48091
|
+
const sourcePentestId = await resolvePentestId(organizationId, options.from);
|
|
48092
|
+
const targetPentestId = await resolvePentestId(organizationId, options.to);
|
|
48093
|
+
const dryRun = options.dryRun ?? false;
|
|
48094
|
+
const body = options.application !== void 0 ? {
|
|
48095
|
+
sourcePentestId,
|
|
48096
|
+
applicationId: await resolveApplicationId(organizationId, options.application),
|
|
48097
|
+
dryRun
|
|
48098
|
+
} : {
|
|
48099
|
+
sourcePentestId,
|
|
48100
|
+
pentestFindingIds: await Promise.all(
|
|
48101
|
+
(options.finding ?? []).map(
|
|
48102
|
+
(id) => resolveFindingId(organizationId, sourcePentestId, id)
|
|
48103
|
+
)
|
|
48104
|
+
),
|
|
48105
|
+
dryRun
|
|
48106
|
+
};
|
|
48107
|
+
const result = await authenticatedRequest(
|
|
48108
|
+
(client) => client.pentestFinding.copyFindings({
|
|
48109
|
+
params: { organizationId, pentestId: targetPentestId },
|
|
48110
|
+
body
|
|
48111
|
+
})
|
|
48112
|
+
);
|
|
48113
|
+
if (result.status !== CREATED_STATUS && result.status !== SUCCESS_STATUS) {
|
|
48114
|
+
failCopy(result.status, result.body);
|
|
48115
|
+
}
|
|
48116
|
+
if (options.output === "json") {
|
|
48117
|
+
printJson(result.body);
|
|
48118
|
+
return;
|
|
48119
|
+
}
|
|
48120
|
+
const { findings } = result.body;
|
|
48121
|
+
if (findings.length === 0) {
|
|
48122
|
+
console.log("No findings matched the selection. Nothing copied.");
|
|
48123
|
+
return;
|
|
48124
|
+
}
|
|
48125
|
+
const exhibits = findings.reduce((n3, f) => n3 + f.evidenceCount, 0);
|
|
48126
|
+
const summary = `${findings.length} finding${findings.length === 1 ? "" : "s"} with ${exhibits} exhibit${exhibits === 1 ? "" : "s"}`;
|
|
48127
|
+
console.log(
|
|
48128
|
+
result.body.dryRun ? `Dry run: ${summary} would be copied from ${sanitizeDisplay(options.from)} to ${sanitizeDisplay(options.to)}.` : colorSuccess(
|
|
48129
|
+
`Copied ${summary} from ${sanitizeDisplay(options.from)} to ${sanitizeDisplay(options.to)}.`
|
|
48130
|
+
)
|
|
48131
|
+
);
|
|
48132
|
+
for (const finding of findings) {
|
|
48133
|
+
console.log(formatCopiedFinding(finding));
|
|
48134
|
+
}
|
|
48135
|
+
if (!result.body.dryRun) {
|
|
48136
|
+
console.log(
|
|
48137
|
+
`The copies are unpublished drafts. Run \`tracecli pentest findings publish --pentest ${sanitizeDisplay(options.to)} --all\` to number them and show them to the customer.`
|
|
48138
|
+
);
|
|
48139
|
+
}
|
|
48140
|
+
}
|
|
48141
|
+
function createCopyFindingsCommand() {
|
|
48142
|
+
return new Command("copy").description(
|
|
48143
|
+
"Copy findings (with prose and evidence) from one pentest into another as unpublished drafts (staff)"
|
|
48144
|
+
).requiredOption("--from <id>", "Source pentest display ID (e.g. PT-1) or UUID").requiredOption("--to <id>", "Target pentest display ID (e.g. PT-2) or UUID").option(
|
|
48145
|
+
"--application <id|name>",
|
|
48146
|
+
"Copy every source finding that cites a repository linked to this application"
|
|
48147
|
+
).option(
|
|
48148
|
+
"--finding <id>",
|
|
48149
|
+
"Source finding number (e.g. PTF-7) or UUID (repeatable)",
|
|
48150
|
+
collectRepeatable
|
|
48151
|
+
).option("--dry-run", "Show what would be copied without writing anything").option("-o, --output <format>", "Output format: text, json (default: text)", "text").action(async (options) => {
|
|
48152
|
+
await copyFindings(options);
|
|
48153
|
+
});
|
|
48154
|
+
}
|
|
48155
|
+
|
|
48156
|
+
// src/pentest/create-finding.ts
|
|
48157
|
+
import { readFileSync as readFileSync6, statSync as statSync3 } from "fs";
|
|
48158
|
+
var SEVERITY_LIST = PentestFindingSeverityValues.join(", ");
|
|
48159
|
+
function collectCwe(value, previous) {
|
|
48160
|
+
return [...previous ?? [], value];
|
|
48161
|
+
}
|
|
48162
|
+
function parseSeverity(raw) {
|
|
48163
|
+
const parsed = PentestFindingSeverity.safeParse(raw.trim().toUpperCase());
|
|
48164
|
+
if (parsed.success) {
|
|
48165
|
+
return parsed.data;
|
|
48166
|
+
}
|
|
48167
|
+
return {
|
|
48168
|
+
error: `Invalid --severity '${sanitizeDisplay(raw)}'. Use one of: ${SEVERITY_LIST}.`
|
|
48169
|
+
};
|
|
48170
|
+
}
|
|
48171
|
+
function parseCvssScore(raw) {
|
|
48172
|
+
const score = Number(raw);
|
|
48173
|
+
if (!Number.isFinite(score) || score < 0 || score > 10) {
|
|
48174
|
+
return {
|
|
48175
|
+
error: `Invalid --cvss-score '${sanitizeDisplay(raw)}'. Expected a number between 0 and 10.`
|
|
48176
|
+
};
|
|
48177
|
+
}
|
|
48178
|
+
return score;
|
|
48179
|
+
}
|
|
48180
|
+
function parseCreateFindingJsonFile(raw, sourceLabel) {
|
|
48181
|
+
let parsed;
|
|
48182
|
+
try {
|
|
48183
|
+
parsed = JSON.parse(raw);
|
|
48184
|
+
} catch {
|
|
48185
|
+
return { error: `Invalid JSON in ${sourceLabel}.` };
|
|
48186
|
+
}
|
|
48187
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
48188
|
+
return {
|
|
48189
|
+
error: `${sourceLabel} must be a JSON object of finding fields (not an array or null).`
|
|
48190
|
+
};
|
|
48191
|
+
}
|
|
48192
|
+
return { data: parsed };
|
|
48193
|
+
}
|
|
48194
|
+
function readUtf8File(path, flag) {
|
|
48195
|
+
try {
|
|
48196
|
+
const stats = statSync3(path);
|
|
48197
|
+
if (!stats.isFile()) {
|
|
48198
|
+
return { error: `${flag} '${sanitizeDisplay(path)}' is not a regular file.` };
|
|
48199
|
+
}
|
|
48200
|
+
} catch {
|
|
48201
|
+
return { error: `Cannot read ${flag} '${sanitizeDisplay(path)}'.` };
|
|
48202
|
+
}
|
|
48203
|
+
const text = readFileSync6(path, "utf8");
|
|
48204
|
+
if (text.trim().length === 0) {
|
|
48205
|
+
return { error: `${flag} '${sanitizeDisplay(path)}' is empty.` };
|
|
48206
|
+
}
|
|
48207
|
+
return { text };
|
|
48208
|
+
}
|
|
48209
|
+
function exclusivePair(value, fileValue, flag) {
|
|
48210
|
+
if (value !== void 0 && fileValue !== void 0) {
|
|
48211
|
+
return { error: `Pass only one of --${flag} / --${flag}-file.` };
|
|
47643
48212
|
}
|
|
47644
48213
|
return { value: fileValue ?? value };
|
|
47645
48214
|
}
|
|
@@ -47859,15 +48428,139 @@ function createCreateFindingCommand() {
|
|
|
47859
48428
|
});
|
|
47860
48429
|
}
|
|
47861
48430
|
|
|
48431
|
+
// src/pentest/staff-errors.ts
|
|
48432
|
+
function failStaffPentestWrite(action, status, body, options = {}) {
|
|
48433
|
+
const { message, errorCode } = readErrorFields(body);
|
|
48434
|
+
if (status === UNAUTHORIZED_STATUS2) {
|
|
48435
|
+
exitAuthFailed();
|
|
48436
|
+
}
|
|
48437
|
+
if (status === 403) {
|
|
48438
|
+
const staffGuard = message === void 0 || /only trace staff/i.test(message);
|
|
48439
|
+
console.error(
|
|
48440
|
+
colorError(
|
|
48441
|
+
staffGuard ? `Only Trace staff can ${action}. Sign in with a staff account via \`tracecli auth login\`.` : `Cannot ${action}: ${message}`
|
|
48442
|
+
)
|
|
48443
|
+
);
|
|
48444
|
+
process.exit(1);
|
|
48445
|
+
}
|
|
48446
|
+
if (status === 404) {
|
|
48447
|
+
console.error(colorError(options.notFound ?? message ?? "Not found."));
|
|
48448
|
+
process.exit(1);
|
|
48449
|
+
}
|
|
48450
|
+
if (status === 400) {
|
|
48451
|
+
console.error(colorError(message ?? "Invalid request (status 400)."));
|
|
48452
|
+
process.exit(1);
|
|
48453
|
+
}
|
|
48454
|
+
if (status === 409) {
|
|
48455
|
+
console.error(
|
|
48456
|
+
colorError(
|
|
48457
|
+
errorCode === "PENTEST_CLOSED" ? options.closed ?? message ?? "Closed pentests must be reopened first." : message ?? "Conflict (status 409)."
|
|
48458
|
+
)
|
|
48459
|
+
);
|
|
48460
|
+
process.exit(1);
|
|
48461
|
+
}
|
|
48462
|
+
console.error(
|
|
48463
|
+
colorError(`Failed to ${action} (status ${status})${message ? `: ${message}` : "."}`)
|
|
48464
|
+
);
|
|
48465
|
+
process.exit(1);
|
|
48466
|
+
}
|
|
48467
|
+
|
|
48468
|
+
// src/pentest/create-pentest.ts
|
|
48469
|
+
var TEST_TYPE_LIST = PentestTestTypeValues.join(", ");
|
|
48470
|
+
function parseTestType(raw) {
|
|
48471
|
+
const parsed = PentestTestType.safeParse(raw.trim().toUpperCase().replace(/-/g, "_"));
|
|
48472
|
+
if (parsed.success) {
|
|
48473
|
+
return parsed.data;
|
|
48474
|
+
}
|
|
48475
|
+
return { error: `Invalid --test-type '${sanitizeDisplay(raw)}'. Use one of: ${TEST_TYPE_LIST}.` };
|
|
48476
|
+
}
|
|
48477
|
+
async function createPentest(options) {
|
|
48478
|
+
const title = options.title.trim();
|
|
48479
|
+
if (title.length === 0) {
|
|
48480
|
+
console.error(colorError("--title cannot be empty."));
|
|
48481
|
+
process.exit(1);
|
|
48482
|
+
}
|
|
48483
|
+
let testType;
|
|
48484
|
+
if (options.testType !== void 0) {
|
|
48485
|
+
const parsed = parseTestType(options.testType);
|
|
48486
|
+
if (typeof parsed !== "string") {
|
|
48487
|
+
console.error(colorError(parsed.error));
|
|
48488
|
+
process.exit(1);
|
|
48489
|
+
}
|
|
48490
|
+
testType = parsed;
|
|
48491
|
+
}
|
|
48492
|
+
const organizationId = await resolveOrganizationId();
|
|
48493
|
+
const primaryApplicationId = options.application === void 0 ? void 0 : await resolveApplicationId(organizationId, options.application);
|
|
48494
|
+
const created = await authenticatedRequest(
|
|
48495
|
+
(client) => client.pentest.createPentest({
|
|
48496
|
+
params: { organizationId },
|
|
48497
|
+
body: {
|
|
48498
|
+
title,
|
|
48499
|
+
...options.description !== void 0 ? { description: options.description } : {},
|
|
48500
|
+
...primaryApplicationId !== void 0 ? { primaryApplicationId } : {},
|
|
48501
|
+
...testType !== void 0 ? { testType } : {}
|
|
48502
|
+
}
|
|
48503
|
+
})
|
|
48504
|
+
);
|
|
48505
|
+
if (created.status !== CREATED_STATUS) {
|
|
48506
|
+
failStaffPentestWrite("create pentests", created.status, created.body);
|
|
48507
|
+
}
|
|
48508
|
+
const pentest = created.body;
|
|
48509
|
+
let hiddenFromCustomerAt = null;
|
|
48510
|
+
if (options.hidden) {
|
|
48511
|
+
const remedy = `Created pentest PT-${pentest.displayId} (${pentest.pentestId}) but could not hide it, so the customer can see it. Run \`tracecli pentest visibility PT-${pentest.displayId} --hidden\`.`;
|
|
48512
|
+
let hidden;
|
|
48513
|
+
try {
|
|
48514
|
+
hidden = await hidePentest(organizationId, pentest.pentestId);
|
|
48515
|
+
} catch (error48) {
|
|
48516
|
+
console.error(
|
|
48517
|
+
colorError(`${remedy} (${error48 instanceof Error ? error48.message : String(error48)})`)
|
|
48518
|
+
);
|
|
48519
|
+
process.exit(1);
|
|
48520
|
+
}
|
|
48521
|
+
if (hidden.status !== SUCCESS_STATUS) {
|
|
48522
|
+
console.error(colorError(`${remedy} (status ${hidden.status})`));
|
|
48523
|
+
process.exit(1);
|
|
48524
|
+
}
|
|
48525
|
+
hiddenFromCustomerAt = hidden.body.hiddenFromCustomerAt;
|
|
48526
|
+
}
|
|
48527
|
+
if (options.output === "json") {
|
|
48528
|
+
printJson({ ...pentest, hiddenFromCustomerAt });
|
|
48529
|
+
return;
|
|
48530
|
+
}
|
|
48531
|
+
console.log(colorSuccess(`Created pentest PT-${pentest.displayId} (${pentest.pentestId})`));
|
|
48532
|
+
console.log(` ${sanitizeDisplay(pentest.title)}`);
|
|
48533
|
+
const applications = (pentest.applications ?? []).map((app) => sanitizeDisplay(app.name));
|
|
48534
|
+
if (applications.length > 0) {
|
|
48535
|
+
console.log(` Application: ${applications.join(", ")}`);
|
|
48536
|
+
}
|
|
48537
|
+
console.log(
|
|
48538
|
+
hiddenFromCustomerAt === null ? " Visible to the customer. Findings stay unpublished until you publish them." : ` Hidden from the customer until you run \`tracecli pentest visibility PT-${pentest.displayId} --visible\`.`
|
|
48539
|
+
);
|
|
48540
|
+
}
|
|
48541
|
+
function hidePentest(organizationId, pentestId) {
|
|
48542
|
+
return authenticatedRequest(
|
|
48543
|
+
(client) => client.staffPentests.setStaffOrganizationPentestCustomerVisibility({
|
|
48544
|
+
params: { organizationId, pentestId },
|
|
48545
|
+
body: { visible: false }
|
|
48546
|
+
})
|
|
48547
|
+
);
|
|
48548
|
+
}
|
|
48549
|
+
function createCreatePentestCommand() {
|
|
48550
|
+
return new Command("create").description("Create a pentest (staff)").requiredOption("--title <text>", "Engagement title").option("--application <id|name>", "Application under test (bound as the primary application)").option("--test-type <type>", `Declared test type (${TEST_TYPE_LIST}); default WHITE_BOX`).option("--description <text>", "Engagement description").option("--hidden", "Hide the pentest from the customer until you make it visible").option("-o, --output <format>", "Output format: text, json (default: text)", "text").action(async (options) => {
|
|
48551
|
+
await createPentest(options);
|
|
48552
|
+
});
|
|
48553
|
+
}
|
|
48554
|
+
|
|
47862
48555
|
// src/pentest/deliverables.ts
|
|
47863
48556
|
import { open as open2, rename, rm, stat } from "fs/promises";
|
|
47864
|
-
import { basename as
|
|
48557
|
+
import { basename as basename4, join as join6, resolve } from "path";
|
|
47865
48558
|
|
|
47866
48559
|
// src/upload/prepare.ts
|
|
47867
48560
|
import { createHash } from "crypto";
|
|
47868
|
-
import { createReadStream, statSync as
|
|
48561
|
+
import { createReadStream, statSync as statSync4 } from "fs";
|
|
47869
48562
|
import { open } from "fs/promises";
|
|
47870
|
-
import { basename as
|
|
48563
|
+
import { basename as basename3 } from "path";
|
|
47871
48564
|
var CodeUploadPrepareError = class extends Error {
|
|
47872
48565
|
constructor(message) {
|
|
47873
48566
|
super(message);
|
|
@@ -47966,7 +48659,7 @@ function assertPreparedMatchesUpload(upload, prepared) {
|
|
|
47966
48659
|
async function prepareUploadFile(filePath, options = {}) {
|
|
47967
48660
|
let sizeBytes;
|
|
47968
48661
|
try {
|
|
47969
|
-
const stats =
|
|
48662
|
+
const stats = statSync4(filePath);
|
|
47970
48663
|
if (!stats.isFile()) {
|
|
47971
48664
|
throw new CodeUploadPrepareError("Path is not a regular file. Pass a .zip, .apk, or .ipa.");
|
|
47972
48665
|
}
|
|
@@ -47981,7 +48674,7 @@ async function prepareUploadFile(filePath, options = {}) {
|
|
|
47981
48674
|
if (sizeBytes > CODE_UPLOAD_MAX_BYTES) {
|
|
47982
48675
|
throw new CodeUploadPrepareError(archiveTooLargeMessage());
|
|
47983
48676
|
}
|
|
47984
|
-
const originalFilename =
|
|
48677
|
+
const originalFilename = basename3(filePath);
|
|
47985
48678
|
const inferred = inferArtifactKindFromFilename(originalFilename);
|
|
47986
48679
|
const artifactKind = options.artifactKind ?? inferred;
|
|
47987
48680
|
if (artifactKind == null) {
|
|
@@ -48228,7 +48921,7 @@ var PentestDeliverableDownloadError = class extends Error {
|
|
|
48228
48921
|
}
|
|
48229
48922
|
};
|
|
48230
48923
|
function safeDeliverableFilename(documentId, extension, fallbackStem) {
|
|
48231
|
-
const stripped = Array.from(
|
|
48924
|
+
const stripped = Array.from(basename4(documentId.replace(/\\/g, "/")), (char) => {
|
|
48232
48925
|
const code = char.codePointAt(0) ?? 0;
|
|
48233
48926
|
return code <= 31 || code === 127 ? "_" : char;
|
|
48234
48927
|
}).join("").trim();
|
|
@@ -48704,191 +49397,15 @@ Examples:
|
|
|
48704
49397
|
tracecli pentest attestation download PT-3 --attestation 6f1c... --out ./loa.pdf --force
|
|
48705
49398
|
tracecli pentest attestation download PT-3 --output json | jq '.path'`
|
|
48706
49399
|
).action(async (pentestId, options) => {
|
|
48707
|
-
await runAttestationDownload(pentestId, options);
|
|
48708
|
-
});
|
|
48709
|
-
}
|
|
48710
|
-
function createAttestationCommand() {
|
|
48711
|
-
const attestation = new Command("attestation").alias("loa").description("List and download letters of attestation");
|
|
48712
|
-
attestation.addCommand(createAttestationListCommand());
|
|
48713
|
-
attestation.addCommand(createAttestationDownloadCommand());
|
|
48714
|
-
return attestation;
|
|
48715
|
-
}
|
|
48716
|
-
|
|
48717
|
-
// src/pentest/evidence.ts
|
|
48718
|
-
import { readFileSync as readFileSync6, statSync as statSync4 } from "fs";
|
|
48719
|
-
import { basename as basename4 } from "path";
|
|
48720
|
-
var SECTION_LIST = ValidationEvidenceSectionValues.join(", ");
|
|
48721
|
-
var KIND_LIST = PentestExploitArtifactKindValues.join(", ");
|
|
48722
|
-
function isSection(value) {
|
|
48723
|
-
return ValidationEvidenceSectionValues.includes(value);
|
|
48724
|
-
}
|
|
48725
|
-
function isKind(value) {
|
|
48726
|
-
return PentestExploitArtifactKindValues.includes(value);
|
|
48727
|
-
}
|
|
48728
|
-
function formatBytes2(bytes) {
|
|
48729
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
48730
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
|
|
48731
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
48732
|
-
}
|
|
48733
|
-
async function resolveFindingId(organizationId, pentestId, identifier) {
|
|
48734
|
-
if (isUuid(identifier)) {
|
|
48735
|
-
return identifier;
|
|
48736
|
-
}
|
|
48737
|
-
const numericMatch = identifier.match(/^(?:PTF-)?(\d+)$/i);
|
|
48738
|
-
if (!numericMatch?.[1]) {
|
|
48739
|
-
console.error(
|
|
48740
|
-
colorError(
|
|
48741
|
-
`Invalid finding identifier '${sanitizeDisplay(identifier)}'. Use a finding number (e.g. PTF-7) or UUID.`
|
|
48742
|
-
)
|
|
48743
|
-
);
|
|
48744
|
-
process.exit(1);
|
|
48745
|
-
}
|
|
48746
|
-
const displayId = Number.parseInt(numericMatch[1], 10);
|
|
48747
|
-
const result = await authenticatedRequest(
|
|
48748
|
-
(client) => client.pentestFinding.listFindings({ params: { organizationId, pentestId } })
|
|
48749
|
-
);
|
|
48750
|
-
if (result.status === UNAUTHORIZED_STATUS2) {
|
|
48751
|
-
exitAuthFailed();
|
|
48752
|
-
}
|
|
48753
|
-
if (result.status !== 200) {
|
|
48754
|
-
console.error(colorError(`Failed to list findings (status ${result.status}).`));
|
|
48755
|
-
process.exit(1);
|
|
48756
|
-
}
|
|
48757
|
-
const match = result.body.find((f) => f.displayId === displayId);
|
|
48758
|
-
if (!match || !("pentestFindingId" in match)) {
|
|
48759
|
-
console.error(
|
|
48760
|
-
colorError(
|
|
48761
|
-
`No finding PTF-${displayId} in this pentest. Unpublished findings have no number yet \u2014 pass the finding UUID instead (\`tracecli pentest findings list --pentest <id> --output json\`).`
|
|
48762
|
-
)
|
|
48763
|
-
);
|
|
48764
|
-
process.exit(1);
|
|
48765
|
-
}
|
|
48766
|
-
return match.pentestFindingId;
|
|
48767
|
-
}
|
|
48768
|
-
async function runAttachEvidence(options) {
|
|
48769
|
-
if (options.section !== void 0 && !isSection(options.section)) {
|
|
48770
|
-
console.error(colorError(`Invalid --section '${sanitizeDisplay(options.section)}'.`));
|
|
48771
|
-
console.error(`Valid sections: ${SECTION_LIST}`);
|
|
48772
|
-
process.exit(1);
|
|
48773
|
-
}
|
|
48774
|
-
if (options.kind !== void 0 && !isKind(options.kind)) {
|
|
48775
|
-
console.error(colorError(`Invalid --kind '${sanitizeDisplay(options.kind)}'.`));
|
|
48776
|
-
console.error(`Valid kinds: ${KIND_LIST}`);
|
|
48777
|
-
process.exit(1);
|
|
48778
|
-
}
|
|
48779
|
-
let sizeBytes;
|
|
48780
|
-
try {
|
|
48781
|
-
const stats = statSync4(options.file);
|
|
48782
|
-
if (!stats.isFile()) {
|
|
48783
|
-
console.error(colorError(`--file '${sanitizeDisplay(options.file)}' is not a regular file.`));
|
|
48784
|
-
process.exit(1);
|
|
48785
|
-
}
|
|
48786
|
-
sizeBytes = stats.size;
|
|
48787
|
-
} catch {
|
|
48788
|
-
console.error(colorError(`Cannot read --file '${sanitizeDisplay(options.file)}'.`));
|
|
48789
|
-
process.exit(1);
|
|
48790
|
-
}
|
|
48791
|
-
if (sizeBytes === 0) {
|
|
48792
|
-
console.error(colorError(`--file '${sanitizeDisplay(options.file)}' is empty.`));
|
|
48793
|
-
process.exit(1);
|
|
48794
|
-
}
|
|
48795
|
-
if (sizeBytes > MAX_STAFF_EVIDENCE_UPLOAD_BYTES) {
|
|
48796
|
-
console.error(
|
|
48797
|
-
colorError(
|
|
48798
|
-
`File is ${formatBytes2(sizeBytes)}; the limit for one exhibit is ${formatBytes2(MAX_STAFF_EVIDENCE_UPLOAD_BYTES)}.`
|
|
48799
|
-
)
|
|
48800
|
-
);
|
|
48801
|
-
process.exit(1);
|
|
48802
|
-
}
|
|
48803
|
-
const organizationId = await resolveOrganizationId();
|
|
48804
|
-
const pentestId = await resolvePentestId(organizationId, options.pentest);
|
|
48805
|
-
const pentestFindingId = await resolveFindingId(organizationId, pentestId, options.finding);
|
|
48806
|
-
const params = { organizationId, pentestId, pentestFindingId };
|
|
48807
|
-
const filename = basename4(options.file);
|
|
48808
|
-
const slot = await authenticatedRequest(
|
|
48809
|
-
(client) => client.pentestFinding.createFindingEvidenceUpload({
|
|
48810
|
-
params,
|
|
48811
|
-
body: { filename, sizeBytes }
|
|
48812
|
-
})
|
|
48813
|
-
);
|
|
48814
|
-
if (slot.status === UNAUTHORIZED_STATUS2) {
|
|
48815
|
-
exitAuthFailed();
|
|
48816
|
-
}
|
|
48817
|
-
if (slot.status !== 201) {
|
|
48818
|
-
const { message } = readErrorFields(slot.body);
|
|
48819
|
-
console.error(
|
|
48820
|
-
colorError(message ?? `Failed to start the evidence upload (status ${slot.status}).`)
|
|
48821
|
-
);
|
|
48822
|
-
process.exit(1);
|
|
48823
|
-
}
|
|
48824
|
-
const body = readFileSync6(options.file);
|
|
48825
|
-
const put = await fetch(slot.body.uploadUrl, { method: "PUT", body: new Uint8Array(body) });
|
|
48826
|
-
if (!put.ok) {
|
|
48827
|
-
console.error(
|
|
48828
|
-
colorError(
|
|
48829
|
-
`Upload to storage failed (status ${put.status}). The upload link is short-lived \u2014 re-run the command.`
|
|
48830
|
-
)
|
|
48831
|
-
);
|
|
48832
|
-
process.exit(1);
|
|
48833
|
-
}
|
|
48834
|
-
const attached = await authenticatedRequest(
|
|
48835
|
-
(client) => client.pentestFinding.attachFindingEvidence({
|
|
48836
|
-
params,
|
|
48837
|
-
body: {
|
|
48838
|
-
key: slot.body.key,
|
|
48839
|
-
...options.caption !== void 0 ? { caption: options.caption } : {},
|
|
48840
|
-
...options.section !== void 0 && isSection(options.section) ? { section: options.section } : {},
|
|
48841
|
-
...options.kind !== void 0 && isKind(options.kind) ? { kind: options.kind } : {},
|
|
48842
|
-
...options.customerFacing ? { customerFacing: true } : {}
|
|
48843
|
-
}
|
|
48844
|
-
})
|
|
48845
|
-
);
|
|
48846
|
-
if (attached.status === UNAUTHORIZED_STATUS2) {
|
|
48847
|
-
exitAuthFailed();
|
|
48848
|
-
}
|
|
48849
|
-
if (attached.status !== 201) {
|
|
48850
|
-
const { message } = readErrorFields(attached.body);
|
|
48851
|
-
console.error(
|
|
48852
|
-
colorError(message ?? `Failed to attach the evidence (status ${attached.status}).`)
|
|
48853
|
-
);
|
|
48854
|
-
process.exit(1);
|
|
48855
|
-
}
|
|
48856
|
-
if (options.output === "json") {
|
|
48857
|
-
printJson(attached.body);
|
|
48858
|
-
return;
|
|
48859
|
-
}
|
|
48860
|
-
const visibility = attached.body.customerFacing ? "customer-facing" : "staff-internal";
|
|
48861
|
-
const placement = attached.body.section ? ` \xB7 ${attached.body.section}` : "";
|
|
48862
|
-
console.log(
|
|
48863
|
-
colorSuccess(
|
|
48864
|
-
`Attached ${attached.body.kind} \xB7 ${formatBytes2(attached.body.sizeBytes)} \xB7 ${visibility}${placement}`
|
|
48865
|
-
)
|
|
48866
|
-
);
|
|
48867
|
-
console.log(` artifact ${attached.body.artifactId}`);
|
|
48868
|
-
if (attached.body.kind === "HAR") {
|
|
48869
|
-
console.log(" HAR captures stay internal \u2014 they can carry a live session token.");
|
|
48870
|
-
} else if (!attached.body.customerFacing) {
|
|
48871
|
-
console.log(" Publish it from the finding\u2019s evidence panel in the staff console.");
|
|
48872
|
-
}
|
|
48873
|
-
}
|
|
48874
|
-
function createAttachEvidenceCommand() {
|
|
48875
|
-
return new Command("attach-evidence").description("Attach a screenshot, recording, or capture to a pentest finding (staff)").requiredOption("--pentest <id>", "Pentest display ID (e.g. PT-3) or UUID").requiredOption("--finding <id>", "Finding number (e.g. PTF-7) or UUID").requiredOption("--file <path>", "Path to the evidence file").option("--caption <text>", "What the exhibit shows; rendered as its label").option("--section <section>", `Report section it backs (${SECTION_LIST})`).option("--kind <kind>", `Override the derived exhibit kind (${KIND_LIST})`).option("--customer-facing", "Publish to the customer (not allowed for HAR or video)").option("-o, --output <format>", "Output format: text, json (default: text)", "text").addHelpText(
|
|
48876
|
-
"after",
|
|
48877
|
-
`
|
|
48878
|
-
The file uploads straight to storage, so recordings of any reasonable size work.
|
|
48879
|
-
Videos and HAR captures always land staff-internal \u2014 publish a video with the
|
|
48880
|
-
toggle in the staff console's evidence panel after reviewing it for redaction.
|
|
48881
|
-
|
|
48882
|
-
Examples:
|
|
48883
|
-
tracecli pentest findings attach-evidence --pentest PT-3 --finding 7 --file ./poc.png
|
|
48884
|
-
tracecli pentest findings attach-evidence --pentest PT-3 --finding PTF-7 --file ./poc.mov \\
|
|
48885
|
-
--caption "Cookie survives logout, replayed 40s later" --section proofOfConcept
|
|
48886
|
-
tracecli pentest findings attach-evidence --pentest PT-3 --finding 7 --file ./roster.json \\
|
|
48887
|
-
--caption "Full member roster returned to a non-member" --customer-facing --output json`
|
|
48888
|
-
).action(async (options) => {
|
|
48889
|
-
await runAttachEvidence(options);
|
|
49400
|
+
await runAttestationDownload(pentestId, options);
|
|
48890
49401
|
});
|
|
48891
49402
|
}
|
|
49403
|
+
function createAttestationCommand() {
|
|
49404
|
+
const attestation = new Command("attestation").alias("loa").description("List and download letters of attestation");
|
|
49405
|
+
attestation.addCommand(createAttestationListCommand());
|
|
49406
|
+
attestation.addCommand(createAttestationDownloadCommand());
|
|
49407
|
+
return attestation;
|
|
49408
|
+
}
|
|
48892
49409
|
|
|
48893
49410
|
// src/pentest/get-finding.ts
|
|
48894
49411
|
var OUTPUT_FORMATS2 = ["text", "json"];
|
|
@@ -48911,7 +49428,9 @@ function sanitizeMarkdown(value) {
|
|
|
48911
49428
|
}
|
|
48912
49429
|
function formatFindingMarkdown(finding) {
|
|
48913
49430
|
const lines = [];
|
|
48914
|
-
|
|
49431
|
+
const unpublished = "publishedAt" in finding && finding.publishedAt === null;
|
|
49432
|
+
const label = finding.displayId === null ? "Draft" : `PTF-${finding.displayId}${unpublished ? " (unpublished)" : ""}`;
|
|
49433
|
+
lines.push(`# ${label}: ${sanitizeDisplay(finding.title)}`);
|
|
48915
49434
|
lines.push("");
|
|
48916
49435
|
lines.push(`- Severity: ${sanitizeDisplay(finding.severity)}`);
|
|
48917
49436
|
lines.push(`- Status: ${sanitizeDisplay(finding.status)}`);
|
|
@@ -48970,6 +49489,18 @@ function failGetFinding(status, body) {
|
|
|
48970
49489
|
);
|
|
48971
49490
|
process.exit(1);
|
|
48972
49491
|
}
|
|
49492
|
+
function readStaffFinding(organizationId, pentestId, pentestFindingId) {
|
|
49493
|
+
return authenticatedRequest(
|
|
49494
|
+
(client) => client.pentestFinding.getFinding({ params: { organizationId, pentestId, pentestFindingId } })
|
|
49495
|
+
);
|
|
49496
|
+
}
|
|
49497
|
+
function readCustomerFinding(organizationId, pentestId, pentestFindingId) {
|
|
49498
|
+
return authenticatedRequest(
|
|
49499
|
+
(client) => client.pentestFinding.getCustomerFinding({
|
|
49500
|
+
params: { organizationId, pentestId, pentestFindingId }
|
|
49501
|
+
})
|
|
49502
|
+
);
|
|
49503
|
+
}
|
|
48973
49504
|
async function runGetFinding(options) {
|
|
48974
49505
|
if (!isOutputFormat2(options.output)) {
|
|
48975
49506
|
console.error(
|
|
@@ -48994,15 +49525,16 @@ async function runGetFinding(options) {
|
|
|
48994
49525
|
process.exit(1);
|
|
48995
49526
|
}
|
|
48996
49527
|
const organizationId = await resolveOrganizationId();
|
|
48997
|
-
const
|
|
48998
|
-
|
|
48999
|
-
|
|
49000
|
-
|
|
49001
|
-
|
|
49002
|
-
|
|
49003
|
-
|
|
49004
|
-
|
|
49005
|
-
|
|
49528
|
+
const me = requireAuth().kind === "user" ? await authenticatedRequestSafe((client) => client.user.getMyContext()) : null;
|
|
49529
|
+
const isStaff = me !== null && me.status === 200 && me.body.user.isStaff;
|
|
49530
|
+
let result;
|
|
49531
|
+
if (isStaff) {
|
|
49532
|
+
const pentestId = await resolvePentestId(organizationId, options.pentest);
|
|
49533
|
+
const pentestFindingId = await resolveFindingId(organizationId, pentestId, options.finding);
|
|
49534
|
+
result = await readStaffFinding(organizationId, pentestId, pentestFindingId);
|
|
49535
|
+
} else {
|
|
49536
|
+
result = await readCustomerFinding(organizationId, options.pentest, options.finding);
|
|
49537
|
+
}
|
|
49006
49538
|
if (result.status !== 200) {
|
|
49007
49539
|
failGetFinding(result.status, result.body);
|
|
49008
49540
|
}
|
|
@@ -49031,6 +49563,185 @@ Examples:
|
|
|
49031
49563
|
});
|
|
49032
49564
|
}
|
|
49033
49565
|
|
|
49566
|
+
// src/pentest/publish-findings.ts
|
|
49567
|
+
var MAX_PUBLISH_BATCH = 200;
|
|
49568
|
+
function validatePublishSelection(options) {
|
|
49569
|
+
const all = options.all === true;
|
|
49570
|
+
const some = options.finding !== void 0 && options.finding.length > 0;
|
|
49571
|
+
if (all === some) {
|
|
49572
|
+
return { error: "Pass exactly one of --all or --finding <uuid> (repeatable)." };
|
|
49573
|
+
}
|
|
49574
|
+
return null;
|
|
49575
|
+
}
|
|
49576
|
+
function isPublishableDraft(finding) {
|
|
49577
|
+
return finding.publishedAt === null && finding.status !== "DISMISSED";
|
|
49578
|
+
}
|
|
49579
|
+
function formatCandidate(finding) {
|
|
49580
|
+
return ` [${sanitizeDisplay(finding.severity)}] ${sanitizeDisplay(finding.title)}`;
|
|
49581
|
+
}
|
|
49582
|
+
function draftsOutrankedBy(selected, all) {
|
|
49583
|
+
const rank = (severity) => SEVERITY_ORDER[severity] ?? Number.MAX_SAFE_INTEGER;
|
|
49584
|
+
const selectedIds = new Set(selected.map((f) => f.pentestFindingId));
|
|
49585
|
+
const leastSevereSelected = Math.max(...selected.map((f) => rank(f.severity)));
|
|
49586
|
+
return all.filter(
|
|
49587
|
+
(f) => !selectedIds.has(f.pentestFindingId) && isPublishableDraft(f) && // A numbered row that was unpublished keeps its number on republish, so it
|
|
49588
|
+
// cannot be pushed out of order by anything published before it.
|
|
49589
|
+
f.displayId === null && rank(f.severity) <= leastSevereSelected
|
|
49590
|
+
).sort((a2, b) => rank(a2.severity) - rank(b.severity));
|
|
49591
|
+
}
|
|
49592
|
+
async function publishFindings(options) {
|
|
49593
|
+
const selection = validatePublishSelection(options);
|
|
49594
|
+
if (selection) {
|
|
49595
|
+
console.error(colorError(selection.error));
|
|
49596
|
+
process.exit(1);
|
|
49597
|
+
}
|
|
49598
|
+
const organizationId = await resolveOrganizationId();
|
|
49599
|
+
const pentestId = await resolvePentestId(organizationId, options.pentest);
|
|
49600
|
+
const pentestResult = await authenticatedRequest(
|
|
49601
|
+
(client) => client.pentest.getPentest({ params: { organizationId, pentestId } })
|
|
49602
|
+
);
|
|
49603
|
+
if (pentestResult.status !== SUCCESS_STATUS) {
|
|
49604
|
+
failStaffPentestWrite("read the pentest", pentestResult.status, pentestResult.body, {
|
|
49605
|
+
notFound: `Pentest '${sanitizeDisplay(options.pentest)}' not found. Verify the ID with \`tracecli pentest list\`.`
|
|
49606
|
+
});
|
|
49607
|
+
}
|
|
49608
|
+
const pentest = pentestResult.body;
|
|
49609
|
+
const label = `PT-${pentest.displayId}`;
|
|
49610
|
+
const listResult = await authenticatedRequest(
|
|
49611
|
+
(client) => client.pentestFinding.listFindings({ params: { organizationId, pentestId } })
|
|
49612
|
+
);
|
|
49613
|
+
if (listResult.status !== SUCCESS_STATUS) {
|
|
49614
|
+
failStaffPentestWrite("list the findings", listResult.status, listResult.body);
|
|
49615
|
+
}
|
|
49616
|
+
const findings = listResult.body;
|
|
49617
|
+
const byId = new Map(findings.map((f) => [f.pentestFindingId, f]));
|
|
49618
|
+
const drafts = findings.filter(isPublishableDraft);
|
|
49619
|
+
let candidates;
|
|
49620
|
+
if (options.all) {
|
|
49621
|
+
candidates = drafts;
|
|
49622
|
+
} else {
|
|
49623
|
+
const draftById = new Map(drafts.map((f) => [f.pentestFindingId, f]));
|
|
49624
|
+
candidates = (options.finding ?? []).map((id) => {
|
|
49625
|
+
const found = draftById.get(id.toLowerCase());
|
|
49626
|
+
if (!found) {
|
|
49627
|
+
const known = byId.get(id.toLowerCase());
|
|
49628
|
+
console.error(
|
|
49629
|
+
colorError(
|
|
49630
|
+
known ? `${sanitizeDisplay(id)} is ${known.publishedAt === null ? "dismissed" : "already published"} in ${label}, so it cannot be published.` : `${sanitizeDisplay(id)} is not an unpublished draft in ${label}. Drafts have no number yet; pass the UUID from \`tracecli pentest findings list --pentest ${label} --output json\`.`
|
|
49631
|
+
)
|
|
49632
|
+
);
|
|
49633
|
+
process.exit(1);
|
|
49634
|
+
}
|
|
49635
|
+
return found;
|
|
49636
|
+
});
|
|
49637
|
+
}
|
|
49638
|
+
if (candidates.length === 0) {
|
|
49639
|
+
if (options.output === "json") {
|
|
49640
|
+
printJson({ published: [] });
|
|
49641
|
+
} else {
|
|
49642
|
+
console.log(
|
|
49643
|
+
`Nothing to publish in ${label}: every non-dismissed finding is already published.`
|
|
49644
|
+
);
|
|
49645
|
+
}
|
|
49646
|
+
return;
|
|
49647
|
+
}
|
|
49648
|
+
if (candidates.length > MAX_PUBLISH_BATCH) {
|
|
49649
|
+
console.error(
|
|
49650
|
+
colorError(
|
|
49651
|
+
`${candidates.length} findings exceed the ${MAX_PUBLISH_BATCH} a single publish accepts. Publish from the staff pentest console for a set this large.`
|
|
49652
|
+
)
|
|
49653
|
+
);
|
|
49654
|
+
process.exit(1);
|
|
49655
|
+
}
|
|
49656
|
+
if (!options.all && options.outOfOrder !== true) {
|
|
49657
|
+
const leftBehind = draftsOutrankedBy(candidates, findings);
|
|
49658
|
+
if (leftBehind.length > 0) {
|
|
49659
|
+
console.error(
|
|
49660
|
+
colorError(
|
|
49661
|
+
`Publishing these now would number them ahead of ${leftBehind.length} unpublished finding${leftBehind.length === 1 ? "" : "s"} of equal or higher severity, and PTF numbers follow severity only when the set is published together:`
|
|
49662
|
+
)
|
|
49663
|
+
);
|
|
49664
|
+
for (const finding of leftBehind) {
|
|
49665
|
+
console.error(formatCandidate(finding));
|
|
49666
|
+
}
|
|
49667
|
+
console.error(
|
|
49668
|
+
colorError(
|
|
49669
|
+
"Use --all to publish them together, or --out-of-order to accept this numbering."
|
|
49670
|
+
)
|
|
49671
|
+
);
|
|
49672
|
+
process.exit(1);
|
|
49673
|
+
}
|
|
49674
|
+
}
|
|
49675
|
+
const hidden = "hiddenFromCustomerAt" in pentest && pentest.hiddenFromCustomerAt != null;
|
|
49676
|
+
if (options.output !== "json" || !options.yes) {
|
|
49677
|
+
console.log(
|
|
49678
|
+
`${candidates.length} finding${candidates.length === 1 ? "" : "s"} will be numbered and shown to the customer in ${label}:`
|
|
49679
|
+
);
|
|
49680
|
+
for (const finding of candidates) {
|
|
49681
|
+
console.log(formatCandidate(finding));
|
|
49682
|
+
}
|
|
49683
|
+
if (hidden) {
|
|
49684
|
+
console.log(
|
|
49685
|
+
colorWarning(
|
|
49686
|
+
`${label} is hidden from the customer, so nothing becomes visible until you run \`tracecli pentest visibility ${label} --visible\`.`
|
|
49687
|
+
)
|
|
49688
|
+
);
|
|
49689
|
+
}
|
|
49690
|
+
}
|
|
49691
|
+
const confirmed = await confirmAction("Publish to the customer?", {
|
|
49692
|
+
skip: options.yes === true,
|
|
49693
|
+
verb: "publish"
|
|
49694
|
+
});
|
|
49695
|
+
if (!confirmed) {
|
|
49696
|
+
console.log("Nothing published.");
|
|
49697
|
+
return;
|
|
49698
|
+
}
|
|
49699
|
+
const result = await authenticatedRequest(
|
|
49700
|
+
(client) => client.pentestFinding.publishAllFindings({
|
|
49701
|
+
params: { organizationId, pentestId },
|
|
49702
|
+
// Always the previewed ids, never the server's default set: a draft that lands
|
|
49703
|
+
// between the preview and the answer must not ride along unseen.
|
|
49704
|
+
body: { pentestFindingIds: candidates.map((f) => f.pentestFindingId) }
|
|
49705
|
+
})
|
|
49706
|
+
);
|
|
49707
|
+
if (result.status !== SUCCESS_STATUS) {
|
|
49708
|
+
failStaffPentestWrite("publish findings", result.status, result.body, {
|
|
49709
|
+
closed: "Closed pentests must be reopened before publishing findings."
|
|
49710
|
+
});
|
|
49711
|
+
}
|
|
49712
|
+
if (options.output === "json") {
|
|
49713
|
+
printJson(result.body);
|
|
49714
|
+
return;
|
|
49715
|
+
}
|
|
49716
|
+
const published = result.body.published;
|
|
49717
|
+
console.log(
|
|
49718
|
+
colorSuccess(
|
|
49719
|
+
`Published ${published.length} finding${published.length === 1 ? "" : "s"} in ${label}.`
|
|
49720
|
+
)
|
|
49721
|
+
);
|
|
49722
|
+
for (const entry of published) {
|
|
49723
|
+
const finding = byId.get(entry.pentestFindingId);
|
|
49724
|
+
console.log(
|
|
49725
|
+
` PTF-${entry.displayId} ${finding ? sanitizeDisplay(finding.title) : entry.pentestFindingId}`
|
|
49726
|
+
);
|
|
49727
|
+
}
|
|
49728
|
+
if (hidden) {
|
|
49729
|
+
console.log(
|
|
49730
|
+
colorWarning(
|
|
49731
|
+
`${label} is still hidden from the customer. Run \`tracecli pentest visibility ${label} --visible\` when you are ready.`
|
|
49732
|
+
)
|
|
49733
|
+
);
|
|
49734
|
+
}
|
|
49735
|
+
}
|
|
49736
|
+
function createPublishFindingsCommand() {
|
|
49737
|
+
return new Command("publish").description("Number unpublished findings and show them to the customer (staff)").requiredOption("--pentest <id>", "Pentest display ID (e.g. PT-3) or UUID").option("--all", "Publish every unpublished, non-dismissed finding").option("--finding <uuid>", "Draft finding UUID to publish (repeatable)", collectRepeatable).option("-y, --yes", "Skip the confirmation prompt").option(
|
|
49738
|
+
"--out-of-order",
|
|
49739
|
+
"With --finding: accept numbering ahead of equal-or-higher-severity drafts left unpublished"
|
|
49740
|
+
).option("-o, --output <format>", "Output format: text, json (default: text)", "text").action(async (options) => {
|
|
49741
|
+
await publishFindings(options);
|
|
49742
|
+
});
|
|
49743
|
+
}
|
|
49744
|
+
|
|
49034
49745
|
// src/pentest/questionnaire.ts
|
|
49035
49746
|
function buildUpsertQuestionnaireBody(answers) {
|
|
49036
49747
|
if (answers === null) {
|
|
@@ -49142,6 +49853,56 @@ function surfaceSelectOptions() {
|
|
|
49142
49853
|
}));
|
|
49143
49854
|
}
|
|
49144
49855
|
|
|
49856
|
+
// src/pentest/visibility.ts
|
|
49857
|
+
function resolveVisibilityChoice(options) {
|
|
49858
|
+
const hidden = options.hidden === true;
|
|
49859
|
+
const visible = options.visible === true;
|
|
49860
|
+
if (hidden === visible) {
|
|
49861
|
+
return { error: "Pass exactly one of --hidden or --visible." };
|
|
49862
|
+
}
|
|
49863
|
+
return { visible };
|
|
49864
|
+
}
|
|
49865
|
+
async function setVisibility(pentestIdentifier, options) {
|
|
49866
|
+
const choice = resolveVisibilityChoice(options);
|
|
49867
|
+
if ("error" in choice) {
|
|
49868
|
+
console.error(colorError(choice.error));
|
|
49869
|
+
process.exit(1);
|
|
49870
|
+
}
|
|
49871
|
+
const organizationId = await resolveOrganizationId();
|
|
49872
|
+
const pentestId = await resolvePentestId(organizationId, pentestIdentifier);
|
|
49873
|
+
const result = await authenticatedRequest(
|
|
49874
|
+
(client) => client.staffPentests.setStaffOrganizationPentestCustomerVisibility({
|
|
49875
|
+
params: { organizationId, pentestId },
|
|
49876
|
+
body: { visible: choice.visible }
|
|
49877
|
+
})
|
|
49878
|
+
);
|
|
49879
|
+
if (result.status !== SUCCESS_STATUS) {
|
|
49880
|
+
failStaffPentestWrite("change pentest visibility", result.status, result.body, {
|
|
49881
|
+
notFound: `Pentest '${sanitizeDisplay(pentestIdentifier)}' not found. Verify the ID with \`tracecli pentest list\`.`
|
|
49882
|
+
});
|
|
49883
|
+
}
|
|
49884
|
+
if (options.output === "json") {
|
|
49885
|
+
printJson(result.body);
|
|
49886
|
+
return;
|
|
49887
|
+
}
|
|
49888
|
+
const label = `PT-${result.body.displayId}`;
|
|
49889
|
+
console.log(
|
|
49890
|
+
colorSuccess(
|
|
49891
|
+
result.body.hiddenFromCustomerAt === null ? `${label} is visible to the customer.` : `${label} is hidden from the customer.`
|
|
49892
|
+
)
|
|
49893
|
+
);
|
|
49894
|
+
if (result.body.hiddenFromCustomerAt === null) {
|
|
49895
|
+
console.log(" Published findings, reports, and attestations show in their dashboard again.");
|
|
49896
|
+
} else {
|
|
49897
|
+
console.log(" The customer no longer sees this pentest anywhere; staff views are unchanged.");
|
|
49898
|
+
}
|
|
49899
|
+
}
|
|
49900
|
+
function createVisibilityCommand() {
|
|
49901
|
+
return new Command("visibility").description("Hide a pentest from the customer or make it visible again (staff)").argument("<pentestId>", "Pentest display ID (e.g. PT-3) or UUID").option("--hidden", "Hide the pentest from the customer").option("--visible", "Make the pentest visible to the customer").option("-o, --output <format>", "Output format: text, json (default: text)", "text").action(async (pentestId, options) => {
|
|
49902
|
+
await setVisibility(pentestId, options);
|
|
49903
|
+
});
|
|
49904
|
+
}
|
|
49905
|
+
|
|
49145
49906
|
// src/pentest/commands.ts
|
|
49146
49907
|
var DEFAULT_LIMIT2 = 30;
|
|
49147
49908
|
var MAX_LIMIT2 = 100;
|
|
@@ -49518,6 +50279,8 @@ function createFindingsCommand() {
|
|
|
49518
50279
|
findings.addCommand(createStatusUpdateCommand());
|
|
49519
50280
|
findings.addCommand(createAttachEvidenceCommand());
|
|
49520
50281
|
findings.addCommand(createCreateFindingCommand(), { hidden: true });
|
|
50282
|
+
findings.addCommand(createCopyFindingsCommand(), { hidden: true });
|
|
50283
|
+
findings.addCommand(createPublishFindingsCommand(), { hidden: true });
|
|
49521
50284
|
return findings;
|
|
49522
50285
|
}
|
|
49523
50286
|
var PRIORITY_SET = new Set(CustomerPriorityCategoryValues);
|
|
@@ -50001,6 +50764,8 @@ function createPentestCommand() {
|
|
|
50001
50764
|
const pentest = new Command("pentest").description("Manage pentests");
|
|
50002
50765
|
pentest.addCommand(createListCommand6());
|
|
50003
50766
|
pentest.addCommand(createUpdateCommand4());
|
|
50767
|
+
pentest.addCommand(createCreatePentestCommand(), { hidden: true });
|
|
50768
|
+
pentest.addCommand(createVisibilityCommand(), { hidden: true });
|
|
50004
50769
|
pentest.addCommand(createQuestionnaireCommand());
|
|
50005
50770
|
pentest.addCommand(createFindingsCommand());
|
|
50006
50771
|
pentest.addCommand(createReportCommand());
|
|
@@ -50008,8 +50773,251 @@ function createPentestCommand() {
|
|
|
50008
50773
|
return pentest;
|
|
50009
50774
|
}
|
|
50010
50775
|
|
|
50776
|
+
// src/test-integration/passkeys.ts
|
|
50777
|
+
import { readFileSync as readFileSync8, unlinkSync } from "fs";
|
|
50778
|
+
|
|
50779
|
+
// src/test-integration/resolve.ts
|
|
50780
|
+
async function resolveTestIntegrationId(label) {
|
|
50781
|
+
const result = await authenticatedRequest(
|
|
50782
|
+
(client) => client.testIntegration.listTestIntegrations()
|
|
50783
|
+
);
|
|
50784
|
+
if (result.status !== SUCCESS_STATUS) {
|
|
50785
|
+
failStaffPentestWrite("list test integrations", result.status, result.body);
|
|
50786
|
+
}
|
|
50787
|
+
const match = result.body.find((integration) => integration.label === label);
|
|
50788
|
+
if (match === void 0) {
|
|
50789
|
+
console.error(
|
|
50790
|
+
colorError(
|
|
50791
|
+
`No test integration named '${sanitizeDisplay(label)}'. Names are exact; the console's Test integrations page lists them.`
|
|
50792
|
+
)
|
|
50793
|
+
);
|
|
50794
|
+
process.exit(1);
|
|
50795
|
+
}
|
|
50796
|
+
return match.testIntegrationId;
|
|
50797
|
+
}
|
|
50798
|
+
|
|
50799
|
+
// src/test-integration/passkeys.ts
|
|
50800
|
+
function parseKeyFile(raw) {
|
|
50801
|
+
let parsed;
|
|
50802
|
+
try {
|
|
50803
|
+
parsed = JSON.parse(raw);
|
|
50804
|
+
} catch {
|
|
50805
|
+
return { error: "The key file is not JSON." };
|
|
50806
|
+
}
|
|
50807
|
+
if (Array.isArray(parsed)) {
|
|
50808
|
+
return {
|
|
50809
|
+
error: "The key file holds an array. Import takes one passkey per file: write the one entry you mean to import as a single object."
|
|
50810
|
+
};
|
|
50811
|
+
}
|
|
50812
|
+
const result = TestIntegrationPasskeyImportSchema.safeParse(parsed);
|
|
50813
|
+
if (!result.success) {
|
|
50814
|
+
const paths = result.error.issues.map((issue2) => issue2.path.map(String).join(".") || "(root)").filter((path, index, all) => all.indexOf(path) === index);
|
|
50815
|
+
return {
|
|
50816
|
+
error: `The key file is not a captured passkey. Problem fields: ${paths.join(", ")}.`
|
|
50817
|
+
};
|
|
50818
|
+
}
|
|
50819
|
+
return { passkey: result.data };
|
|
50820
|
+
}
|
|
50821
|
+
function describePasskey(passkey) {
|
|
50822
|
+
const user = passkey.userName ?? void 0;
|
|
50823
|
+
return user === void 0 || user.length === 0 ? `the ${sanitizeDisplay(passkey.rpId)} passkey` : `the ${sanitizeDisplay(passkey.rpId)} passkey for ${sanitizeDisplay(user)}`;
|
|
50824
|
+
}
|
|
50825
|
+
function importSuccessLine(input) {
|
|
50826
|
+
const head = `Imported ${describePasskey(input.passkey)} into ${sanitizeDisplay(input.label)} (id ${input.id}).`;
|
|
50827
|
+
return input.deleted ? `${head} Deleted the key file ${input.path}.` : `${head} Kept ${input.path}. It holds the private key, so delete it once every environment you need has it, or pass --delete-file on the last import.`;
|
|
50828
|
+
}
|
|
50829
|
+
function deleteSuccessLine(input) {
|
|
50830
|
+
return `Deleted ${describePasskey(input.passkey)} from ${sanitizeDisplay(input.label)}. The site still lists it on the account, so remove it there too, and do it now if the key may have leaked.`;
|
|
50831
|
+
}
|
|
50832
|
+
async function importPasskey(label, options) {
|
|
50833
|
+
let raw;
|
|
50834
|
+
try {
|
|
50835
|
+
raw = readFileSync8(options.file, "utf8");
|
|
50836
|
+
} catch {
|
|
50837
|
+
console.error(colorError(`Cannot read the key file ${options.file}.`));
|
|
50838
|
+
process.exit(1);
|
|
50839
|
+
}
|
|
50840
|
+
const parsed = parseKeyFile(raw);
|
|
50841
|
+
if ("error" in parsed) {
|
|
50842
|
+
console.error(colorError(parsed.error));
|
|
50843
|
+
process.exit(1);
|
|
50844
|
+
}
|
|
50845
|
+
const { passkey } = parsed;
|
|
50846
|
+
const testIntegrationId = await resolveTestIntegrationId(label);
|
|
50847
|
+
if (options.output !== "json") {
|
|
50848
|
+
console.log(`Integration ${sanitizeDisplay(label)}`);
|
|
50849
|
+
console.log(`Site ${sanitizeDisplay(passkey.rpId)}`);
|
|
50850
|
+
console.log(
|
|
50851
|
+
`User ${passkey.userName ? sanitizeDisplay(passkey.userName) : colorMuted("(none in file)")}`
|
|
50852
|
+
);
|
|
50853
|
+
console.log(`Counter ${passkey.signCount}`);
|
|
50854
|
+
}
|
|
50855
|
+
const confirmed = await confirmAction(`Import this passkey into ${sanitizeDisplay(label)}?`, {
|
|
50856
|
+
skip: options.yes === true,
|
|
50857
|
+
verb: "import"
|
|
50858
|
+
});
|
|
50859
|
+
if (!confirmed) {
|
|
50860
|
+
console.log("Nothing imported.");
|
|
50861
|
+
return;
|
|
50862
|
+
}
|
|
50863
|
+
const result = await authenticatedRequest(
|
|
50864
|
+
(client) => client.testIntegration.importTestIntegrationPasskey({
|
|
50865
|
+
params: { testIntegrationId },
|
|
50866
|
+
body: passkey
|
|
50867
|
+
})
|
|
50868
|
+
);
|
|
50869
|
+
if (result.status !== CREATED_STATUS) {
|
|
50870
|
+
if (result.status === 409) {
|
|
50871
|
+
const { message } = readErrorFields(result.body);
|
|
50872
|
+
console.error(colorError(message ?? "This passkey is already stored on the integration."));
|
|
50873
|
+
console.error(`The key file ${options.file} was kept.`);
|
|
50874
|
+
process.exit(1);
|
|
50875
|
+
}
|
|
50876
|
+
failStaffPentestWrite("import a passkey", result.status, result.body, {
|
|
50877
|
+
notFound: `No test integration named '${sanitizeDisplay(label)}'.`
|
|
50878
|
+
});
|
|
50879
|
+
}
|
|
50880
|
+
let deleted = false;
|
|
50881
|
+
if (options.deleteFile === true) {
|
|
50882
|
+
unlinkSync(options.file);
|
|
50883
|
+
deleted = true;
|
|
50884
|
+
}
|
|
50885
|
+
if (options.output === "json") {
|
|
50886
|
+
printJson({ ...result.body, keyFile: deleted ? null : options.file });
|
|
50887
|
+
return;
|
|
50888
|
+
}
|
|
50889
|
+
console.log(
|
|
50890
|
+
colorSuccess(
|
|
50891
|
+
importSuccessLine({
|
|
50892
|
+
passkey,
|
|
50893
|
+
label,
|
|
50894
|
+
id: result.body.testIntegrationPasskeyId,
|
|
50895
|
+
path: options.file,
|
|
50896
|
+
deleted
|
|
50897
|
+
})
|
|
50898
|
+
)
|
|
50899
|
+
);
|
|
50900
|
+
}
|
|
50901
|
+
async function listPasskeys(label, options) {
|
|
50902
|
+
const testIntegrationId = await resolveTestIntegrationId(label);
|
|
50903
|
+
const result = await authenticatedRequest(
|
|
50904
|
+
(client) => client.testIntegration.listTestIntegrationPasskeys({ params: { testIntegrationId } })
|
|
50905
|
+
);
|
|
50906
|
+
if (result.status !== SUCCESS_STATUS) {
|
|
50907
|
+
failStaffPentestWrite("list passkeys", result.status, result.body);
|
|
50908
|
+
}
|
|
50909
|
+
const rows = result.body;
|
|
50910
|
+
if (options.output === "json") {
|
|
50911
|
+
printJson(rows);
|
|
50912
|
+
return;
|
|
50913
|
+
}
|
|
50914
|
+
if (options.output === "tsv") {
|
|
50915
|
+
console.log("id site user imported lastUsed");
|
|
50916
|
+
for (const row of rows) {
|
|
50917
|
+
console.log(
|
|
50918
|
+
[
|
|
50919
|
+
row.testIntegrationPasskeyId,
|
|
50920
|
+
row.rpId,
|
|
50921
|
+
userColumn(row),
|
|
50922
|
+
toIso(row.createdAt),
|
|
50923
|
+
row.lastAssertedAt === null ? "" : toIso(row.lastAssertedAt)
|
|
50924
|
+
].join(" ")
|
|
50925
|
+
);
|
|
50926
|
+
}
|
|
50927
|
+
return;
|
|
50928
|
+
}
|
|
50929
|
+
if (rows.length === 0) {
|
|
50930
|
+
console.log(`No passkeys stored for ${sanitizeDisplay(label)}.`);
|
|
50931
|
+
return;
|
|
50932
|
+
}
|
|
50933
|
+
for (const row of rows) {
|
|
50934
|
+
console.log(
|
|
50935
|
+
`${row.testIntegrationPasskeyId} ${sanitizeDisplay(row.rpId)} ${userColumn(row)} imported ${toIso(row.createdAt)} last used ${row.lastAssertedAt === null ? "never" : toIso(row.lastAssertedAt)}`
|
|
50936
|
+
);
|
|
50937
|
+
}
|
|
50938
|
+
}
|
|
50939
|
+
function userColumn(row) {
|
|
50940
|
+
if (row.unreadable) return "unreadable";
|
|
50941
|
+
return row.userName === null ? "" : sanitizeDisplay(row.userName);
|
|
50942
|
+
}
|
|
50943
|
+
async function deletePasskey(label, passkeyId, options) {
|
|
50944
|
+
const testIntegrationId = await resolveTestIntegrationId(label);
|
|
50945
|
+
const listed = await authenticatedRequest(
|
|
50946
|
+
(client) => client.testIntegration.listTestIntegrationPasskeys({ params: { testIntegrationId } })
|
|
50947
|
+
);
|
|
50948
|
+
if (listed.status !== SUCCESS_STATUS) {
|
|
50949
|
+
failStaffPentestWrite("list passkeys", listed.status, listed.body);
|
|
50950
|
+
}
|
|
50951
|
+
const row = listed.body.find(
|
|
50952
|
+
(r2) => r2.testIntegrationPasskeyId === passkeyId
|
|
50953
|
+
);
|
|
50954
|
+
if (row === void 0) {
|
|
50955
|
+
console.error(
|
|
50956
|
+
colorError(
|
|
50957
|
+
`No passkey with id ${sanitizeDisplay(passkeyId)} is stored on ${sanitizeDisplay(label)}. Run \`tracecli test-integration passkeys list ${sanitizeDisplay(label)}\`.`
|
|
50958
|
+
)
|
|
50959
|
+
);
|
|
50960
|
+
process.exit(1);
|
|
50961
|
+
}
|
|
50962
|
+
const confirmed = await confirmAction(
|
|
50963
|
+
`Delete ${describePasskey(row)} from ${sanitizeDisplay(label)}?`,
|
|
50964
|
+
{ skip: options.yes === true, verb: "delete" }
|
|
50965
|
+
);
|
|
50966
|
+
if (!confirmed) {
|
|
50967
|
+
console.log("Nothing deleted.");
|
|
50968
|
+
return;
|
|
50969
|
+
}
|
|
50970
|
+
const result = await authenticatedRequest(
|
|
50971
|
+
(client) => client.testIntegration.deleteTestIntegrationPasskey({
|
|
50972
|
+
params: { testIntegrationId, testIntegrationPasskeyId: passkeyId }
|
|
50973
|
+
})
|
|
50974
|
+
);
|
|
50975
|
+
if (result.status !== NO_CONTENT_STATUS) {
|
|
50976
|
+
failStaffPentestWrite("delete a passkey", result.status, result.body, {
|
|
50977
|
+
notFound: `No passkey with id ${sanitizeDisplay(passkeyId)} is stored on ${sanitizeDisplay(label)}.`
|
|
50978
|
+
});
|
|
50979
|
+
}
|
|
50980
|
+
if (options.output === "json") {
|
|
50981
|
+
printJson({ deleted: passkeyId });
|
|
50982
|
+
return;
|
|
50983
|
+
}
|
|
50984
|
+
console.log(colorSuccess(deleteSuccessLine({ passkey: row, label })));
|
|
50985
|
+
}
|
|
50986
|
+
function createPasskeysCommand() {
|
|
50987
|
+
const passkeys = new Command("passkeys").description(
|
|
50988
|
+
"Passkeys stored on a test integration (staff)"
|
|
50989
|
+
);
|
|
50990
|
+
passkeys.addCommand(
|
|
50991
|
+
new Command("import").description("Store a passkey the capture script wrote (staff)").argument("<label>", "The test integration name, exactly as the console shows it").requiredOption("--file <path>", "The captured key file (one passkey)").option("--delete-file", "Delete the key file after a successful import").option("-y, --yes", "Skip the confirmation").option("-o, --output <format>", "Output format: text, json (default: text)", "text").action(async (label, options) => {
|
|
50992
|
+
await importPasskey(label, options);
|
|
50993
|
+
})
|
|
50994
|
+
);
|
|
50995
|
+
passkeys.addCommand(
|
|
50996
|
+
new Command("list").description("List a test integration's stored passkeys (staff)").argument("<label>", "The test integration name").option("-o, --output <format>", "Output format: text, json, tsv (default: text)", "text").action(async (label, options) => {
|
|
50997
|
+
await listPasskeys(label, options);
|
|
50998
|
+
})
|
|
50999
|
+
);
|
|
51000
|
+
passkeys.addCommand(
|
|
51001
|
+
new Command("delete").description("Remove one stored passkey from a test integration (staff)").argument("<label>", "The test integration name").argument("<passkeyId>", "The passkey id from `passkeys list`").option("-y, --yes", "Skip the confirmation").option("-o, --output <format>", "Output format: text, json (default: text)", "text").action(
|
|
51002
|
+
async (label, passkeyId, options) => {
|
|
51003
|
+
await deletePasskey(label, passkeyId, options);
|
|
51004
|
+
}
|
|
51005
|
+
)
|
|
51006
|
+
);
|
|
51007
|
+
return passkeys;
|
|
51008
|
+
}
|
|
51009
|
+
|
|
51010
|
+
// src/test-integration/commands.ts
|
|
51011
|
+
function createTestIntegrationCommand() {
|
|
51012
|
+
const group = new Command("test-integration").description(
|
|
51013
|
+
"Trace-owned test integrations (staff)"
|
|
51014
|
+
);
|
|
51015
|
+
group.addCommand(createPasskeysCommand());
|
|
51016
|
+
return group;
|
|
51017
|
+
}
|
|
51018
|
+
|
|
50011
51019
|
// src/update/commands.ts
|
|
50012
|
-
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as
|
|
51020
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
|
|
50013
51021
|
import { dirname as dirname3, join as join7 } from "path";
|
|
50014
51022
|
import { createInterface as createInterface4 } from "readline/promises";
|
|
50015
51023
|
var UPDATE_CACHE_FILE = "update-check.json";
|
|
@@ -50027,7 +51035,7 @@ function readUpdateCheckCache() {
|
|
|
50027
51035
|
return null;
|
|
50028
51036
|
}
|
|
50029
51037
|
try {
|
|
50030
|
-
const rawCache =
|
|
51038
|
+
const rawCache = readFileSync9(cachePath, "utf-8");
|
|
50031
51039
|
const parsed = JSON.parse(rawCache);
|
|
50032
51040
|
if (typeof parsed !== "object" || parsed === null) {
|
|
50033
51041
|
return null;
|
|
@@ -51858,6 +52866,7 @@ program2.addCommand(createVulnerabilitiesCommand());
|
|
|
51858
52866
|
program2.addCommand(createCodeSizeCommand());
|
|
51859
52867
|
program2.addCommand(createUploadCommand());
|
|
51860
52868
|
program2.addCommand(createPentestCommand());
|
|
52869
|
+
program2.addCommand(createTestIntegrationCommand(), { hidden: true });
|
|
51861
52870
|
program2.addCommand(createCompletionCommand(program2));
|
|
51862
52871
|
program2.addCommand(createUpgradeCommand());
|
|
51863
52872
|
await program2.parseAsync();
|