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