securewithtrace 0.33.0 → 0.34.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.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/dist/index.js +996 -176
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -23871,6 +23871,48 @@ var OrganizationCodeSizeResponseSchema = external_exports.object({
23871
23871
  languageCount: external_exports.number().int().nonnegative()
23872
23872
  })
23873
23873
  });
23874
+ var StaffCodeSizeRepositorySchema = external_exports.object({
23875
+ repositoryId: external_exports.uuid(),
23876
+ repositoryUrl: external_exports.url(),
23877
+ platform: external_exports.enum(CodeSizePlatformValues),
23878
+ /** Resolved branch the measurement is keyed on (scanningBranch ?? default). */
23879
+ branch: external_exports.string(),
23880
+ /** null = no successful measurement for the current resolved branch. */
23881
+ billableSloc: external_exports.number().int().nonnegative().nullable(),
23882
+ /** null exactly when `billableSloc` is null. */
23883
+ measuredAt: external_exports.iso.datetime().nullable()
23884
+ });
23885
+ var StaffOrganizationCodeSizeResponseSchema = external_exports.object({
23886
+ /** Same server-side sort as the customer wire: measured first by
23887
+ * billableSloc desc (ties repositoryUrl asc), then unmeasured by
23888
+ * repositoryUrl asc. */
23889
+ repositories: external_exports.array(StaffCodeSizeRepositorySchema),
23890
+ // No language breakdown here on purpose: this panel answers "is every
23891
+ // repository measured, and how fresh", and the language view already exists
23892
+ // on the customer panel staff can impersonate into.
23893
+ totals: external_exports.object({
23894
+ billableSloc: external_exports.number().int().nonnegative(),
23895
+ rawLines: external_exports.number().int().nonnegative(),
23896
+ /** Connected GitHub/Bitbucket repos — the re-measure `all` scope size. */
23897
+ repositoryCount: external_exports.number().int().nonnegative(),
23898
+ /** Repos with a measurement for their current resolved branch. */
23899
+ measuredCount: external_exports.number().int().nonnegative(),
23900
+ /** Newest measuredAt across the org; null when nothing is measured. */
23901
+ lastMeasuredAt: external_exports.iso.datetime().nullable()
23902
+ })
23903
+ });
23904
+ var StaffCodeSizeRemeasureScopeValues = ["all", "unmeasured"];
23905
+ var StaffCodeSizeRemeasureRequestSchema = external_exports.object({
23906
+ scope: external_exports.enum(StaffCodeSizeRemeasureScopeValues)
23907
+ });
23908
+ var StaffCodeSizeRemeasureResponseSchema = external_exports.object({
23909
+ scope: external_exports.enum(StaffCodeSizeRemeasureScopeValues),
23910
+ /** Repos the scope selected. */
23911
+ requested: external_exports.number().int().nonnegative(),
23912
+ /** Messages SQS accepted. Lower than `requested` means a partial send —
23913
+ * reported rather than masked (the panel states this number). */
23914
+ enqueued: external_exports.number().int().nonnegative()
23915
+ });
23874
23916
 
23875
23917
  // ../../packages/api-contracts/src/contracts/v1/code-size.ts
23876
23918
  var c8 = initContract();
@@ -23886,6 +23928,36 @@ var codeSizeContract = c8.router({
23886
23928
  404: ErrorResponseSchema,
23887
23929
  500: ErrorResponseSchema
23888
23930
  }
23931
+ },
23932
+ // Staff-only: per-repo measurement freshness for the admin Controls panel.
23933
+ getStaffOrganizationCodeSize: {
23934
+ method: "GET",
23935
+ path: "/staff/organizations/:organizationId/code-size",
23936
+ pathParams: OrgIdParamsSchema,
23937
+ responses: {
23938
+ 200: StaffOrganizationCodeSizeResponseSchema,
23939
+ 401: ErrorResponseSchema,
23940
+ 403: ErrorResponseSchema,
23941
+ 404: ErrorResponseSchema,
23942
+ 500: ErrorResponseSchema
23943
+ }
23944
+ },
23945
+ // Staff-only: enqueue measurements for the scoped repos. Enqueue-only — the
23946
+ // worker measures asynchronously, so a 200 means "requested", not "measured".
23947
+ remeasureStaffOrganizationCodeSize: {
23948
+ method: "POST",
23949
+ path: "/staff/organizations/:organizationId/code-size/re-measure",
23950
+ pathParams: OrgIdParamsSchema,
23951
+ body: StaffCodeSizeRemeasureRequestSchema,
23952
+ responses: {
23953
+ 200: StaffCodeSizeRemeasureResponseSchema,
23954
+ 401: ErrorResponseSchema,
23955
+ 403: ErrorResponseSchema,
23956
+ 404: ErrorResponseSchema,
23957
+ 500: ErrorResponseSchema,
23958
+ // Measurement enqueueing is unconfigured (REPO_METADATA_QUEUE_URL unset).
23959
+ 503: ErrorResponseSchema
23960
+ }
23889
23961
  }
23890
23962
  });
23891
23963
 
@@ -27287,11 +27359,19 @@ var CustomerPentestDetailResponseSchema = CustomerPentestResponseSchema.extend({
27287
27359
  * GET …/questionnaire. `.optional()` for rolling-deploy safety.
27288
27360
  */
27289
27361
  questionnaireEditable: external_exports.boolean().optional(),
27290
- hasQuestionnaireAnswers: external_exports.boolean().optional()
27362
+ hasQuestionnaireAnswers: external_exports.boolean().optional(),
27363
+ /**
27364
+ * True once the customer has submitted the questionnaire (frozen). Optional
27365
+ * for rolling-deploy safety against an older API that does not send it.
27366
+ */
27367
+ questionnaireSubmitted: external_exports.boolean().optional()
27291
27368
  });
27292
27369
  var CustomerPentestQuestionnaireResponseSchema = external_exports.object({
27293
27370
  pentestId: external_exports.uuid(),
27294
- /** True iff currentStage ∈ SCOPING_PENTEST_STAGES (DRAFT | READY_TO_START). */
27371
+ /**
27372
+ * True iff the engagement is still in a scoping stage AND the questionnaire
27373
+ * has not been submitted. A submitted intake is frozen even during scoping.
27374
+ */
27295
27375
  editable: external_exports.boolean(),
27296
27376
  /**
27297
27377
  * Lenient on unknown keys: during a rolling deploy the dashboard runs against
@@ -27300,7 +27380,13 @@ var CustomerPentestQuestionnaireResponseSchema = external_exports.object({
27300
27380
  * strict — see the upsert request below.
27301
27381
  */
27302
27382
  answers: PentestCustomerIntakeStoredAnswersSchema.nullable(),
27303
- updatedAt: external_exports.coerce.date()
27383
+ updatedAt: external_exports.coerce.date(),
27384
+ /**
27385
+ * When the customer submitted (froze) the questionnaire. Null = still a
27386
+ * draft. Optional so an older API response still parses during a rolling
27387
+ * deploy; absence means "not submitted" to new clients.
27388
+ */
27389
+ submittedAt: external_exports.coerce.date().nullable().optional()
27304
27390
  });
27305
27391
  var UpsertCustomerPentestQuestionnaireRequestSchema = external_exports.object({
27306
27392
  /**
@@ -27310,6 +27396,14 @@ var UpsertCustomerPentestQuestionnaireRequestSchema = external_exports.object({
27310
27396
  */
27311
27397
  answers: PentestCustomerIntakeWriteAnswersSchema.nullable()
27312
27398
  }).strict();
27399
+ var SubmitCustomerPentestQuestionnaireRequestSchema = external_exports.object({
27400
+ /**
27401
+ * Optional last draft to persist atomically with the freeze. Omitted keeps
27402
+ * whatever is already stored. Retired keys are accepted and dropped, same
27403
+ * as the upsert path.
27404
+ */
27405
+ answers: PentestCustomerIntakeWriteAnswersSchema.optional()
27406
+ }).strict();
27313
27407
  var ReadPentestResponseSchema = external_exports.union([
27314
27408
  PentestDetailResponseSchema,
27315
27409
  CustomerPentestShellResponseSchema
@@ -27525,6 +27619,21 @@ var pentestContract = c22.router({
27525
27619
  500: ErrorResponseSchema
27526
27620
  }
27527
27621
  },
27622
+ submitCustomerPentestQuestionnaire: {
27623
+ method: "POST",
27624
+ path: "/organizations/:organizationId/customer-pentests/:pentestId/questionnaire/submit",
27625
+ pathParams: CustomerPentestIdParamsSchema,
27626
+ body: SubmitCustomerPentestQuestionnaireRequestSchema,
27627
+ responses: {
27628
+ 200: CustomerPentestQuestionnaireResponseSchema,
27629
+ 400: ErrorResponseSchema,
27630
+ 401: ErrorResponseSchema,
27631
+ 403: ErrorResponseSchema,
27632
+ 404: ErrorResponseSchema,
27633
+ 409: ErrorResponseSchema,
27634
+ 500: ErrorResponseSchema
27635
+ }
27636
+ },
27528
27637
  createPentest: {
27529
27638
  method: "POST",
27530
27639
  path: "/organizations/:organizationId/pentests",
@@ -29074,6 +29183,25 @@ var CurateFindingEvidenceRequestSchema = external_exports.object({
29074
29183
  }).strict().refine((v2) => Object.keys(v2).length > 0, {
29075
29184
  message: "At least one curation field must be provided"
29076
29185
  });
29186
+ var MAX_STAFF_EVIDENCE_UPLOAD_BYTES = 256 * 1024 * 1024;
29187
+ var CreateFindingEvidenceUploadRequestSchema = external_exports.object({
29188
+ filename: external_exports.string().min(1).max(255),
29189
+ sizeBytes: external_exports.number().int().positive().max(MAX_STAFF_EVIDENCE_UPLOAD_BYTES)
29190
+ }).strict();
29191
+ var CreateFindingEvidenceUploadResponseSchema = external_exports.object({
29192
+ key: external_exports.string(),
29193
+ uploadUrl: external_exports.url(),
29194
+ expiresAt: external_exports.coerce.date(),
29195
+ kind: PentestExploitArtifactKind,
29196
+ mimeType: external_exports.string()
29197
+ });
29198
+ var AttachFindingEvidenceRequestSchema = external_exports.object({
29199
+ key: external_exports.string().min(1).max(1024),
29200
+ kind: PentestExploitArtifactKind.optional(),
29201
+ caption: external_exports.string().min(1).max(500).optional(),
29202
+ section: ValidationEvidenceSection.optional(),
29203
+ customerFacing: external_exports.boolean().optional()
29204
+ }).strict();
29077
29205
  var PentestFindingRepositorySchema = external_exports.object({
29078
29206
  repositoryId: external_exports.uuid(),
29079
29207
  repositoryName: external_exports.string(),
@@ -29680,6 +29808,43 @@ var pentestFindingContract = c28.router({
29680
29808
  500: ErrorResponseSchema
29681
29809
  }
29682
29810
  },
29811
+ // ─── Evidence (staff manual attach) ───
29812
+ //
29813
+ // Two calls, because the bytes never pass through the api: mint a presigned
29814
+ // PUT, upload straight to S3, then register the object as an exhibit. That
29815
+ // keeps a screen-recorded PoC off the request path entirely (no body limit, no
29816
+ // request timeout, no api memory) and mirrors how the operator agent session
29817
+ // already attaches evidence.
29818
+ createFindingEvidenceUpload: {
29819
+ method: "POST",
29820
+ path: "/organizations/:organizationId/pentests/:pentestId/findings/:pentestFindingId/evidence/uploads",
29821
+ pathParams: PentestFindingIdParamsSchema,
29822
+ body: CreateFindingEvidenceUploadRequestSchema,
29823
+ responses: {
29824
+ 201: CreateFindingEvidenceUploadResponseSchema,
29825
+ 400: ErrorResponseSchema,
29826
+ 401: ErrorResponseSchema,
29827
+ 403: ErrorResponseSchema,
29828
+ 404: ErrorResponseSchema,
29829
+ 409: ErrorResponseSchema,
29830
+ 500: ErrorResponseSchema
29831
+ }
29832
+ },
29833
+ attachFindingEvidence: {
29834
+ method: "POST",
29835
+ path: "/organizations/:organizationId/pentests/:pentestId/findings/:pentestFindingId/evidence",
29836
+ pathParams: PentestFindingIdParamsSchema,
29837
+ body: AttachFindingEvidenceRequestSchema,
29838
+ responses: {
29839
+ 201: PentestExploitArtifactMetadataResponseSchema,
29840
+ 400: ErrorResponseSchema,
29841
+ 401: ErrorResponseSchema,
29842
+ 403: ErrorResponseSchema,
29843
+ 404: ErrorResponseSchema,
29844
+ 409: ErrorResponseSchema,
29845
+ 500: ErrorResponseSchema
29846
+ }
29847
+ },
29683
29848
  curateFindingEvidence: {
29684
29849
  method: "PATCH",
29685
29850
  path: "/organizations/:organizationId/pentests/:pentestId/findings/:pentestFindingId/evidence/:artifactId",
@@ -30060,6 +30225,12 @@ var PentestHypothesisAttemptSummarySchema = external_exports.object({
30060
30225
  status: PentestHypothesisAttemptStatus,
30061
30226
  attemptOrdinal: external_exports.number().int().positive(),
30062
30227
  conclusion: external_exports.string().nullable(),
30228
+ // Optional for rolling-deploy safety: web (Vercel) can deploy this schema before the
30229
+ // API sends the field, and web strictly `.parse()`s the response. Absent → undefined,
30230
+ // which the web serializer coalesces to null. Carries the Blocked/Failed reason so the
30231
+ // hypothesis drawer's verdict panel can show it when the deciding attempt is only in
30232
+ // hand as this summary (a verdict from a prior run).
30233
+ blockerReason: external_exports.string().nullable().optional(),
30063
30234
  startedAt: external_exports.coerce.date().nullable(),
30064
30235
  completedAt: external_exports.coerce.date().nullable(),
30065
30236
  durationMs: external_exports.number().int().nonnegative().nullable(),
@@ -38860,6 +39031,194 @@ function showBanner() {
38860
39031
  `);
38861
39032
  }
38862
39033
 
39034
+ // src/code-size/format.ts
39035
+ function formatCount(value) {
39036
+ return value.toLocaleString("en-US");
39037
+ }
39038
+ function repoDisplayName(repositoryUrl) {
39039
+ try {
39040
+ const path = new URL(repositoryUrl).pathname.replace(/^\//, "").replace(/\.git$/, "");
39041
+ return path || repositoryUrl;
39042
+ } catch {
39043
+ return repositoryUrl;
39044
+ }
39045
+ }
39046
+ function countedRows(totals) {
39047
+ const b2 = totals.buckets;
39048
+ return [
39049
+ { label: "Billable code", value: b2.code.code },
39050
+ { label: "Blank & comment lines in billable files", value: b2.code.lines - b2.code.code },
39051
+ { label: "Tests", value: b2.tests.lines },
39052
+ { label: "Documentation", value: b2.docs.lines },
39053
+ { label: "Configuration & data", value: b2.config_data.lines },
39054
+ { label: "Vendored & generated", value: b2.vendored_generated.lines }
39055
+ ];
39056
+ }
39057
+ function countedTotal(totals) {
39058
+ return countedRows(totals).reduce((sum, row) => sum + row.value, 0);
39059
+ }
39060
+ function padValues(rows) {
39061
+ const formatted = rows.map((row) => formatCount(row.value));
39062
+ const labelWidth = Math.max(...rows.map((row) => row.label.length));
39063
+ const valueWidth = Math.max(...formatted.map((value) => value.length));
39064
+ return rows.map(
39065
+ (row, i) => ` ${row.label.padEnd(labelWidth)} ${(formatted[i] ?? "").padStart(valueWidth)}`
39066
+ );
39067
+ }
39068
+ function renderCodeSizeTable(codeSize, options) {
39069
+ const { repositories, totals } = codeSize;
39070
+ const measured = repositories.filter((repo) => repo.billableSloc !== null);
39071
+ const lines = [`Code size for ${options.orgSlug}`, ""];
39072
+ if (measured.length === 0) {
39073
+ lines.push(
39074
+ " No measurements yet.",
39075
+ ` ${formatCount(repositories.length)} connected ${plural(repositories.length, "repository has", "repositories have")} not been measured.`
39076
+ );
39077
+ return lines;
39078
+ }
39079
+ lines.push(
39080
+ ` ${formatCount(totals.billableSloc)} billable SLOC`,
39081
+ ` of ${formatCount(totals.rawLines)} total lines across ${formatCount(measured.length)} measured ${plural(measured.length, "repository", "repositories")}`
39082
+ );
39083
+ const unmeasured = repositories.length - measured.length;
39084
+ if (unmeasured > 0) {
39085
+ lines.push(
39086
+ ` ${formatCount(unmeasured)} connected ${plural(unmeasured, "repository is", "repositories are")} not measured yet and not included`
39087
+ );
39088
+ }
39089
+ lines.push("", "What's counted", ...padValues(countedRows(totals)));
39090
+ const counted = countedTotal(totals);
39091
+ if (counted !== totals.rawLines) {
39092
+ lines.push(
39093
+ ` These sum to ${formatCount(counted)}, not the ${formatCount(totals.rawLines)} above.`,
39094
+ " Some measurement predates the current counting rules; re-measure to reconcile."
39095
+ );
39096
+ }
39097
+ lines.push("", "By repository");
39098
+ const shown = measured.slice(0, options.repoLimit);
39099
+ lines.push(
39100
+ ...padValues(
39101
+ shown.map((repo) => ({
39102
+ label: repoDisplayName(repo.repositoryUrl),
39103
+ value: repo.billableSloc ?? 0
39104
+ }))
39105
+ )
39106
+ );
39107
+ const hidden = measured.length - shown.length;
39108
+ if (hidden > 0) {
39109
+ lines.push(` ... ${formatCount(hidden)} more`);
39110
+ }
39111
+ return lines;
39112
+ }
39113
+ function renderCodeSizeTsv(codeSize) {
39114
+ return [
39115
+ "repository billableSloc",
39116
+ ...codeSize.repositories.map(
39117
+ (repo) => `${repoDisplayName(repo.repositoryUrl)} ${repo.billableSloc === null ? "" : repo.billableSloc}`
39118
+ )
39119
+ ];
39120
+ }
39121
+ function plural(count, one, many) {
39122
+ return count === 1 ? one : many;
39123
+ }
39124
+
39125
+ // src/code-size/commands.ts
39126
+ var DEFAULT_REPO_LIMIT = 20;
39127
+ var OUTPUT_FORMATS = ["table", "json", "tsv"];
39128
+ function isOutputFormat(value) {
39129
+ return OUTPUT_FORMATS.includes(value);
39130
+ }
39131
+ function parseRepoLimit(value) {
39132
+ if (!/^\d+$/.test(value.trim())) {
39133
+ console.error("--repos must be a non-negative whole number (0 shows every repository).");
39134
+ process.exit(1);
39135
+ }
39136
+ return Number.parseInt(value, 10);
39137
+ }
39138
+ function createCodeSizeCommand() {
39139
+ return new Command("code-size").description("Show the organization's billable lines of code").option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").option(
39140
+ "-n, --repos <count>",
39141
+ `Per-repository rows to show in table output (default: ${DEFAULT_REPO_LIMIT}, 0 for all)`,
39142
+ parseRepoLimit
39143
+ ).option("-w, --web", "Open the code size panel in a browser").addHelpText(
39144
+ "after",
39145
+ `
39146
+ Billable SLOC is counted from each connected repository's current scanning
39147
+ branch. Blanks, comments, tests, docs, config and vendored code are excluded;
39148
+ "What's counted" reconciles the billable number to what plain scc reports.
39149
+ See ${getAppBaseUrl()}/docs/integrations/code-size
39150
+
39151
+ Examples:
39152
+ tracecli code-size
39153
+ tracecli code-size --repos 0
39154
+ tracecli code-size --output json | jq '.totals.billableSloc'
39155
+ tracecli code-size --output tsv | cut -f1,2
39156
+ tracecli code-size --web`
39157
+ ).action(async (options) => {
39158
+ if (!isOutputFormat(options.output)) {
39159
+ console.error(
39160
+ `Unsupported --output '${options.output}'. Expected one of: ${OUTPUT_FORMATS.join(", ")}.`
39161
+ );
39162
+ process.exit(1);
39163
+ }
39164
+ const listed = await authenticatedRequest(
39165
+ (client) => client.organization.listMyOrganizations()
39166
+ );
39167
+ if (listed.status === 401) {
39168
+ console.error("Authentication failed. Run `tracecli auth login` to re-authenticate.");
39169
+ process.exit(1);
39170
+ }
39171
+ if (listed.status !== 200) {
39172
+ console.error(`Failed to resolve organization (status ${listed.status}).`);
39173
+ process.exit(1);
39174
+ }
39175
+ const resolution = determineActiveOrganization(
39176
+ listed.body,
39177
+ readSelectedOrganizationId(),
39178
+ extractOrganizationIdFromToken(readToken())
39179
+ );
39180
+ if (!resolution) {
39181
+ console.error("No organization found for your account.");
39182
+ process.exit(1);
39183
+ }
39184
+ const { organizationId, slug } = resolution.organization;
39185
+ if (options.web) {
39186
+ const url2 = `${getAppBaseUrl()}/dashboard/${slug}/settings/integrations/github`;
39187
+ const opened = await openBrowser(url2);
39188
+ if (!opened) {
39189
+ console.log(`Open in browser: ${url2}`);
39190
+ }
39191
+ return;
39192
+ }
39193
+ const result = await authenticatedRequest(
39194
+ (client) => client.codeSize.getOrganizationCodeSize({ params: { organizationId } })
39195
+ );
39196
+ if (result.status === 401) {
39197
+ console.error("Authentication failed. Run `tracecli auth login` to re-authenticate.");
39198
+ process.exit(1);
39199
+ }
39200
+ if (result.status !== 200) {
39201
+ console.error(`Failed to read code size (status ${result.status}).`);
39202
+ process.exit(1);
39203
+ }
39204
+ const codeSize = result.body;
39205
+ if (options.output === "json") {
39206
+ console.log(JSON.stringify(codeSize, null, 2));
39207
+ return;
39208
+ }
39209
+ if (options.output === "tsv") {
39210
+ for (const line of renderCodeSizeTsv(codeSize)) {
39211
+ console.log(line);
39212
+ }
39213
+ return;
39214
+ }
39215
+ const repoLimit = options.repos === void 0 ? DEFAULT_REPO_LIMIT : options.repos === 0 ? Number.POSITIVE_INFINITY : options.repos;
39216
+ for (const line of renderCodeSizeTable(codeSize, { orgSlug: slug, repoLimit })) {
39217
+ console.log(line);
39218
+ }
39219
+ });
39220
+ }
39221
+
38863
39222
  // src/completion/index.ts
38864
39223
  function extractCommandTree(cmd) {
38865
39224
  const BUILTIN_FLAGS = /* @__PURE__ */ new Set(["--help", "-h", "--version", "-V"]);
@@ -39231,73 +39590,318 @@ Examples:
39231
39590
  });
39232
39591
  }
39233
39592
 
39234
- // src/secret-input.ts
39235
- import { readFileSync as readFileSync2 } from "fs";
39236
- var SecretInputError = class extends Error {
39237
- };
39238
- function secretSourceCount(opts) {
39239
- return [opts.stdin === true, opts.file !== void 0, opts.env !== void 0].filter(Boolean).length;
39240
- }
39241
- function usesStdin(opts) {
39242
- return opts.stdin === true;
39593
+ // src/inbox/commands.ts
39594
+ function validateInboxLabel(label) {
39595
+ const parsed = InboxLabelSchema.safeParse(label);
39596
+ if (parsed.success) {
39597
+ return null;
39598
+ }
39599
+ return "Inbox label must be lowercase letters, digits, or dashes, starting with a letter (max 40 characters).";
39243
39600
  }
39244
- function stripTrailingNewline(value) {
39245
- return value.replace(/\r?\n$/, "");
39601
+ function buildCreateInboxBody(label) {
39602
+ return { label };
39246
39603
  }
39247
- async function defaultReadStdin() {
39248
- const chunks = [];
39249
- for await (const chunk of process.stdin) {
39250
- chunks.push(Buffer.from(chunk));
39604
+ function failWriteOp2(action, status, body, notFoundLabel) {
39605
+ const { message, errorCode } = readErrorFields(body);
39606
+ if (status === UNAUTHORIZED_STATUS2) {
39607
+ exitAuthFailed();
39251
39608
  }
39252
- return Buffer.concat(chunks).toString("utf8");
39253
- }
39254
- async function resolveSecret(label, flagBase, opts, readStdin = defaultReadStdin) {
39255
- if (secretSourceCount(opts) > 1) {
39256
- throw new SecretInputError(
39257
- `Provide only one source for ${label} (--${flagBase}-stdin / --${flagBase}-file / --${flagBase}-env).`
39609
+ if (status === 403) {
39610
+ console.error(
39611
+ colorError(
39612
+ `Only an organization admin (or Trace staff) on a pentest-enabled organization can ${action} inboxes.`
39613
+ )
39258
39614
  );
39615
+ process.exit(1);
39259
39616
  }
39260
- if (opts.stdin === true) {
39261
- const value = stripTrailingNewline(await readStdin());
39262
- if (value.length === 0) {
39263
- throw new SecretInputError(`No ${label} received on stdin.`);
39264
- }
39265
- return value;
39617
+ if (status === 404) {
39618
+ console.error(
39619
+ colorError(message ?? (notFoundLabel ? `No inbox labeled '${notFoundLabel}'.` : "Not found."))
39620
+ );
39621
+ process.exit(1);
39266
39622
  }
39267
- if (opts.file !== void 0) {
39268
- let contents;
39269
- try {
39270
- contents = readFileSync2(opts.file, "utf8");
39271
- } catch {
39272
- throw new SecretInputError(`Could not read ${label} file: ${opts.file}`);
39273
- }
39274
- const value = stripTrailingNewline(contents);
39275
- if (value.length === 0) {
39276
- throw new SecretInputError(`${label} file is empty: ${opts.file}`);
39277
- }
39278
- return value;
39623
+ if (status === 400) {
39624
+ console.error(colorError(message ?? "Invalid request (status 400)."));
39625
+ process.exit(1);
39279
39626
  }
39280
- if (opts.env !== void 0) {
39281
- const value = process.env[opts.env];
39282
- if (value === void 0 || value.length === 0) {
39283
- throw new SecretInputError(
39284
- `Environment variable ${opts.env} for ${label} is unset or empty.`
39627
+ if (status === 409) {
39628
+ if (errorCode === "INBOX_ALREADY_EXISTS") {
39629
+ console.error(
39630
+ colorError(message ?? "An inbox with this label already exists in this organization.")
39631
+ );
39632
+ } else if (errorCode === "INBOX_BEING_DELETED") {
39633
+ console.error(
39634
+ colorError(
39635
+ message ?? "That name was recently removed and is still being cleaned up. Try again shortly, or choose another."
39636
+ )
39637
+ );
39638
+ } else if (errorCode === "INBOX_QUOTA_EXCEEDED") {
39639
+ console.error(
39640
+ colorError(message ?? "Inbox limit reached. Delete an unused inbox to create a new one.")
39285
39641
  );
39642
+ } else {
39643
+ console.error(colorError(message ?? "Conflict (status 409)."));
39286
39644
  }
39287
- return value;
39645
+ process.exit(1);
39288
39646
  }
39289
- if (!process.stdin.isTTY || !process.stdout.isTTY) {
39290
- throw new SecretInputError(
39291
- `No source for ${label}. Use --${flagBase}-stdin, --${flagBase}-file, or --${flagBase}-env (or run interactively to be prompted).`
39647
+ if (status === 503) {
39648
+ console.error(
39649
+ colorError(
39650
+ message ?? "Managed inboxes are not enabled on this server: no mail provider is configured."
39651
+ )
39292
39652
  );
39653
+ process.exit(1);
39293
39654
  }
39294
- const entered = await Ce({ message: `Enter ${label}:` });
39295
- if (q(entered) || typeof entered !== "string" || entered.length === 0) {
39296
- throw new SecretInputError(`${label} entry was cancelled or empty.`);
39297
- }
39298
- return entered;
39655
+ console.error(
39656
+ colorError(`Failed to ${action} inbox (status ${status})${message ? `: ${message}` : "."}`)
39657
+ );
39658
+ process.exit(1);
39299
39659
  }
39300
-
39660
+ function failReadOp2(status, body) {
39661
+ const { message } = readErrorFields(body);
39662
+ if (status === UNAUTHORIZED_STATUS2) {
39663
+ exitAuthFailed();
39664
+ }
39665
+ if (status === 403) {
39666
+ console.error(colorError("You don't have access to view inboxes in this organization."));
39667
+ process.exit(1);
39668
+ }
39669
+ if (status === 503) {
39670
+ console.error(
39671
+ colorError(
39672
+ message ?? "Managed inboxes are not enabled on this server: no mail provider is configured."
39673
+ )
39674
+ );
39675
+ process.exit(1);
39676
+ }
39677
+ console.error(
39678
+ colorError(`Failed to list inboxes (status ${status})${message ? `: ${message}` : "."}`)
39679
+ );
39680
+ process.exit(1);
39681
+ }
39682
+ function renderInboxDetail(inbox) {
39683
+ const pad = 10;
39684
+ return [
39685
+ `${"Address:".padEnd(pad)} ${sanitizeDisplay(inbox.address)}`,
39686
+ `${"Label:".padEnd(pad)} ${sanitizeDisplay(inbox.label)}`,
39687
+ `${"Created:".padEnd(pad)} ${formatDate(inbox.createdAt)}`
39688
+ ].join("\n");
39689
+ }
39690
+ function printInboxesTsv(inboxes) {
39691
+ console.log(["address", "label", "createdAt"].join(" "));
39692
+ for (const inbox of inboxes) {
39693
+ console.log(
39694
+ [
39695
+ tsvSafe(sanitizeDisplay(inbox.address)),
39696
+ tsvSafe(sanitizeDisplay(inbox.label)),
39697
+ tsvSafe(toIso(inbox.createdAt))
39698
+ ].join(" ")
39699
+ );
39700
+ }
39701
+ }
39702
+ function buildInboxesTable(inboxes) {
39703
+ const cells = inboxes.map((inbox) => ({
39704
+ address: sanitizeDisplay(inbox.address),
39705
+ label: sanitizeDisplay(inbox.label)
39706
+ }));
39707
+ const addressWidth = Math.max(7, ...cells.map((c51) => c51.address.length));
39708
+ const header = `${"ADDRESS".padEnd(addressWidth)} LABEL`;
39709
+ const rows = cells.map((c51) => `${c51.address.padEnd(addressWidth)} ${c51.label}`);
39710
+ return { header, rows };
39711
+ }
39712
+ function formatInboxQuota(quota) {
39713
+ return `${quota.used} of ${quota.limit} inboxes`;
39714
+ }
39715
+ function outputInbox(inbox, output) {
39716
+ if (output === "json") {
39717
+ console.log(JSON.stringify(inbox, null, 2));
39718
+ return;
39719
+ }
39720
+ if (output === "tsv") {
39721
+ printInboxesTsv([inbox]);
39722
+ return;
39723
+ }
39724
+ console.log(renderInboxDetail(inbox));
39725
+ }
39726
+ function requireInboxLabel(label) {
39727
+ const error48 = validateInboxLabel(label);
39728
+ if (error48) {
39729
+ console.error(colorError(error48));
39730
+ process.exit(1);
39731
+ }
39732
+ }
39733
+ async function runCreate2(label, options) {
39734
+ const output = parseOutput(options.output);
39735
+ requireInboxLabel(label);
39736
+ const organizationId = await resolveOrganizationId();
39737
+ const result = await authenticatedRequest(
39738
+ (client) => client.inbox.createInbox({
39739
+ params: { organizationId },
39740
+ body: buildCreateInboxBody(label)
39741
+ })
39742
+ );
39743
+ if (result.status !== CREATED_STATUS) {
39744
+ failWriteOp2("create", result.status, result.body);
39745
+ }
39746
+ if (output === "table") {
39747
+ console.log(colorSuccess(`Created inbox ${sanitizeDisplay(result.body.address)}`));
39748
+ }
39749
+ outputInbox(result.body, output);
39750
+ }
39751
+ async function runList2(options) {
39752
+ const output = parseOutput(options.output);
39753
+ const organizationId = await resolveOrganizationId();
39754
+ const result = await authenticatedRequest(
39755
+ (client) => client.inbox.listInboxes({ params: { organizationId } })
39756
+ );
39757
+ if (result.status !== SUCCESS_STATUS) {
39758
+ failReadOp2(result.status, result.body);
39759
+ }
39760
+ const { inboxes, quota } = result.body;
39761
+ if (output === "json") {
39762
+ console.log(JSON.stringify({ inboxes, quota }, null, 2));
39763
+ return;
39764
+ }
39765
+ if (output === "tsv") {
39766
+ printInboxesTsv(inboxes);
39767
+ return;
39768
+ }
39769
+ if (inboxes.length === 0) {
39770
+ console.log(`No inboxes found. ${formatInboxQuota(quota)}.`);
39771
+ return;
39772
+ }
39773
+ const { header, rows } = buildInboxesTable(inboxes);
39774
+ await runInteractivePager({
39775
+ items: inboxes,
39776
+ header,
39777
+ renderItem: (_inbox, i) => [rows[i] ?? ""],
39778
+ disabled: options.pager === false
39779
+ });
39780
+ console.log(formatInboxQuota(quota));
39781
+ }
39782
+ async function runDelete2(label, options) {
39783
+ requireInboxLabel(label);
39784
+ const confirmed = await confirmDeletion(`inbox ${label}`, options.yes);
39785
+ if (!confirmed) {
39786
+ console.log("Aborted.");
39787
+ return;
39788
+ }
39789
+ const organizationId = await resolveOrganizationId();
39790
+ const result = await authenticatedRequest(
39791
+ (client) => client.inbox.deleteInbox({
39792
+ params: { organizationId, label }
39793
+ })
39794
+ );
39795
+ if (result.status !== NO_CONTENT_STATUS) {
39796
+ failWriteOp2("delete", result.status, result.body, label);
39797
+ }
39798
+ console.log(colorSuccess(`Deleted inbox ${sanitizeDisplay(label)}`));
39799
+ }
39800
+ function createCreateCommand2() {
39801
+ return new Command("create").description("Provision a Trace-hosted email inbox").argument("<label>", "Inbox name (lowercase letters/digits/dashes, max 40)").option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").addHelpText(
39802
+ "after",
39803
+ `
39804
+ Trace hosts this inbox; the pentest agent reads its emails automatically.
39805
+ The minted address is {org-slug}-{label}@pentest.securewithtrace.com.
39806
+
39807
+ Use the printed address as the test-account email when creating a magic-auth
39808
+ or username-password credential.
39809
+
39810
+ Examples:
39811
+ tracecli credential inbox create portal
39812
+ ADDRESS=$(tracecli credential inbox create qa-admin -o json | jq -r .address)
39813
+ echo "$ADDRESS" | tracecli credential create --env <envId> --type magic-auth --secret-stdin`
39814
+ ).action(async (label, options) => {
39815
+ await runCreate2(label, options);
39816
+ });
39817
+ }
39818
+ function createListCommand3() {
39819
+ return new Command("list").description("List Trace-hosted email inboxes in the active organization").option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").option("--no-pager", "Disable the interactive pager for table output").action(async (options) => {
39820
+ await runList2(options);
39821
+ });
39822
+ }
39823
+ function createDeleteCommand2() {
39824
+ return new Command("delete").description("Delete a Trace-hosted email inbox").argument("<label>", "Inbox label").option("-y, --yes", "Skip the confirmation prompt (required in non-interactive shells)").action(async (label, options) => {
39825
+ await runDelete2(label, options);
39826
+ });
39827
+ }
39828
+ function createInboxCommand() {
39829
+ const inbox = new Command("inbox").description(
39830
+ "Provision Trace-hosted email inboxes for test-account credentials"
39831
+ );
39832
+ inbox.addCommand(createCreateCommand2());
39833
+ inbox.addCommand(createListCommand3());
39834
+ inbox.addCommand(createDeleteCommand2());
39835
+ return inbox;
39836
+ }
39837
+
39838
+ // src/secret-input.ts
39839
+ import { readFileSync as readFileSync2 } from "fs";
39840
+ var SecretInputError = class extends Error {
39841
+ };
39842
+ function secretSourceCount(opts) {
39843
+ return [opts.stdin === true, opts.file !== void 0, opts.env !== void 0].filter(Boolean).length;
39844
+ }
39845
+ function usesStdin(opts) {
39846
+ return opts.stdin === true;
39847
+ }
39848
+ function stripTrailingNewline(value) {
39849
+ return value.replace(/\r?\n$/, "");
39850
+ }
39851
+ async function defaultReadStdin() {
39852
+ const chunks = [];
39853
+ for await (const chunk of process.stdin) {
39854
+ chunks.push(Buffer.from(chunk));
39855
+ }
39856
+ return Buffer.concat(chunks).toString("utf8");
39857
+ }
39858
+ async function resolveSecret(label, flagBase, opts, readStdin = defaultReadStdin) {
39859
+ if (secretSourceCount(opts) > 1) {
39860
+ throw new SecretInputError(
39861
+ `Provide only one source for ${label} (--${flagBase}-stdin / --${flagBase}-file / --${flagBase}-env).`
39862
+ );
39863
+ }
39864
+ if (opts.stdin === true) {
39865
+ const value = stripTrailingNewline(await readStdin());
39866
+ if (value.length === 0) {
39867
+ throw new SecretInputError(`No ${label} received on stdin.`);
39868
+ }
39869
+ return value;
39870
+ }
39871
+ if (opts.file !== void 0) {
39872
+ let contents;
39873
+ try {
39874
+ contents = readFileSync2(opts.file, "utf8");
39875
+ } catch {
39876
+ throw new SecretInputError(`Could not read ${label} file: ${opts.file}`);
39877
+ }
39878
+ const value = stripTrailingNewline(contents);
39879
+ if (value.length === 0) {
39880
+ throw new SecretInputError(`${label} file is empty: ${opts.file}`);
39881
+ }
39882
+ return value;
39883
+ }
39884
+ if (opts.env !== void 0) {
39885
+ const value = process.env[opts.env];
39886
+ if (value === void 0 || value.length === 0) {
39887
+ throw new SecretInputError(
39888
+ `Environment variable ${opts.env} for ${label} is unset or empty.`
39889
+ );
39890
+ }
39891
+ return value;
39892
+ }
39893
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
39894
+ throw new SecretInputError(
39895
+ `No source for ${label}. Use --${flagBase}-stdin, --${flagBase}-file, or --${flagBase}-env (or run interactively to be prompted).`
39896
+ );
39897
+ }
39898
+ const entered = await Ce({ message: `Enter ${label}:` });
39899
+ if (q(entered) || typeof entered !== "string" || entered.length === 0) {
39900
+ throw new SecretInputError(`${label} entry was cancelled or empty.`);
39901
+ }
39902
+ return entered;
39903
+ }
39904
+
39301
39905
  // src/credential/commands.ts
39302
39906
  var CREDENTIAL_TYPE_BY_INPUT = {
39303
39907
  "bearer-token": "BEARER_TOKEN",
@@ -39523,7 +40127,7 @@ function usernameOpts(opts) {
39523
40127
  function passwordOpts(opts) {
39524
40128
  return { stdin: opts.passwordStdin, file: opts.passwordFile, env: opts.passwordEnv };
39525
40129
  }
39526
- function failWriteOp2(action, status, body, notFoundLabel) {
40130
+ function failWriteOp3(action, status, body, notFoundLabel) {
39527
40131
  const { message, errorCode } = readErrorFields(body);
39528
40132
  if (status === UNAUTHORIZED_STATUS2) {
39529
40133
  exitAuthFailed();
@@ -39567,7 +40171,7 @@ function failWriteOp2(action, status, body, notFoundLabel) {
39567
40171
  );
39568
40172
  process.exit(1);
39569
40173
  }
39570
- function failReadOp2(action, status, body, notFoundLabel) {
40174
+ function failReadOp3(action, status, body, notFoundLabel) {
39571
40175
  const { message } = readErrorFields(body);
39572
40176
  if (status === UNAUTHORIZED_STATUS2) {
39573
40177
  exitAuthFailed();
@@ -39659,7 +40263,7 @@ function failAuthBuild(result) {
39659
40263
  console.error(colorError(result.error));
39660
40264
  process.exit(1);
39661
40265
  }
39662
- async function runCreate2(options) {
40266
+ async function runCreate3(options) {
39663
40267
  const output = parseOutput(options.output);
39664
40268
  requireEnvId(options.env);
39665
40269
  const type = normalizeCredentialType(options.type);
@@ -39699,7 +40303,7 @@ async function runCreate2(options) {
39699
40303
  (client) => client.credential.createCredential({ params, body })
39700
40304
  );
39701
40305
  if (result.status !== CREATED_STATUS) {
39702
- failWriteOp2("create", result.status, result.body);
40306
+ failWriteOp3("create", result.status, result.body);
39703
40307
  }
39704
40308
  if (output === "table") {
39705
40309
  console.log(colorSuccess(`Created credential ${sanitizeDisplay(result.body.label)}`));
@@ -39791,7 +40395,7 @@ async function buildCreateBody(type, label, options) {
39791
40395
  }
39792
40396
  }
39793
40397
  }
39794
- async function runList2(options) {
40398
+ async function runList3(options) {
39795
40399
  const output = parseOutput(options.output);
39796
40400
  requireEnvId(options.env);
39797
40401
  const organizationId = await resolveOrganizationId();
@@ -39799,7 +40403,7 @@ async function runList2(options) {
39799
40403
  (client) => client.credential.listCredentials({ params: { organizationId, environmentId: options.env } })
39800
40404
  );
39801
40405
  if (result.status !== SUCCESS_STATUS) {
39802
- failReadOp2("list credentials", result.status, result.body, options.env);
40406
+ failReadOp3("list credentials", result.status, result.body, options.env);
39803
40407
  }
39804
40408
  const creds = result.body;
39805
40409
  if (output === "json") {
@@ -39833,7 +40437,7 @@ async function runGet2(credentialId, options) {
39833
40437
  })
39834
40438
  );
39835
40439
  if (result.status !== SUCCESS_STATUS) {
39836
- failReadOp2("get credential", result.status, result.body, credentialId);
40440
+ failReadOp3("get credential", result.status, result.body, credentialId);
39837
40441
  }
39838
40442
  outputCredential(result.body, output);
39839
40443
  }
@@ -39867,7 +40471,7 @@ async function runUpdate2(credentialId, options) {
39867
40471
  (client) => client.credential.getCredential({ params })
39868
40472
  );
39869
40473
  if (current.status !== SUCCESS_STATUS) {
39870
- failReadOp2("get credential", current.status, current.body, credentialId);
40474
+ failReadOp3("get credential", current.status, current.body, credentialId);
39871
40475
  }
39872
40476
  const type = current.body.credentialType;
39873
40477
  const foreign = rejectForeignAuthFlags(type, options);
@@ -39900,14 +40504,14 @@ async function runUpdate2(credentialId, options) {
39900
40504
  (client) => client.credential.updateCredential({ params, body })
39901
40505
  );
39902
40506
  if (result.status !== SUCCESS_STATUS) {
39903
- failWriteOp2("update", result.status, result.body, credentialId);
40507
+ failWriteOp3("update", result.status, result.body, credentialId);
39904
40508
  }
39905
40509
  if (output === "table") {
39906
40510
  console.log(colorSuccess(`Updated credential ${sanitizeDisplay(result.body.label)}`));
39907
40511
  }
39908
40512
  outputCredential(result.body, output);
39909
40513
  }
39910
- async function runDelete2(credentialId, options) {
40514
+ async function runDelete3(credentialId, options) {
39911
40515
  requireEnvId(options.env);
39912
40516
  requireCredId(credentialId);
39913
40517
  const confirmed = await confirmDeletion(`credential ${credentialId}`, options.yes);
@@ -39922,7 +40526,7 @@ async function runDelete2(credentialId, options) {
39922
40526
  })
39923
40527
  );
39924
40528
  if (result.status !== NO_CONTENT_STATUS) {
39925
- failWriteOp2("delete", result.status, result.body, credentialId);
40529
+ failWriteOp3("delete", result.status, result.body, credentialId);
39926
40530
  }
39927
40531
  console.log(colorSuccess(`Deleted credential ${credentialId}`));
39928
40532
  }
@@ -39932,7 +40536,7 @@ function addSecretInputOptions(command) {
39932
40536
  function addAuthConfigOptions(command) {
39933
40537
  return command.option("--header-name <name>", "authConfig: header name (bearer-token/jwt-token)").option("--token-prefix <prefix>", "authConfig: token prefix (bearer-token/jwt-token)").option("--placement <header|query>", "authConfig: API key placement (api-key)").option("--key-name <name>", "authConfig: API key name (api-key)").option("--cookie-name <name>", "authConfig: cookie name (cookie)").option("--cookie-domain <domain>", "authConfig: cookie domain (cookie)").option("--cookie-path <path>", "authConfig: cookie path (cookie)").option("--method <form|basic|json>", "authConfig: login method (username-password)").option("--login-url <url>", "authConfig: login URL (username-password)").option("--username-field <name>", "authConfig: username field (username-password)").option("--password-field <name>", "authConfig: password field (username-password)").option("--session-cookie-name <name>", "authConfig: session cookie (username-password)").option("--csrf-token-selector <sel>", "authConfig: CSRF token selector (username-password)").option("--csrf-field-name <name>", "authConfig: CSRF field name (username-password)");
39934
40538
  }
39935
- function createCreateCommand2() {
40539
+ function createCreateCommand3() {
39936
40540
  const command = new Command("create").description("Create a credential in an environment").requiredOption("--env <environmentId>", "Parent environment UUID").requiredOption(
39937
40541
  "--type <type>",
39938
40542
  "bearer-token, jwt-token, api-key, passphrase, username-password, cookie, magic-auth"
@@ -39950,15 +40554,17 @@ Secrets are read from stdin/file/env (or a no-echo prompt) \u2014 never an inlin
39950
40554
  Examples:
39951
40555
  vault read -field=token secret/x | tracecli credential create --env <envId> --type bearer-token --secret-stdin
39952
40556
  tracecli credential create --env <envId> --type api-key --label "prod key" --secret-env API_KEY --placement header --key-name X-API-Key
39953
- tracecli credential create --env <envId> --type username-password --username-env U --password-env P --method form --login-url https://acme.com/login`
40557
+ tracecli credential create --env <envId> --type username-password --username-env U --password-env P --method form --login-url https://acme.com/login
40558
+ ADDRESS=$(tracecli credential inbox create portal -o json | jq -r .address)
40559
+ echo "$ADDRESS" | tracecli credential create --env <envId> --type magic-auth --secret-stdin`
39954
40560
  );
39955
40561
  return command.action(async (options) => {
39956
- await runCreate2(options);
40562
+ await runCreate3(options);
39957
40563
  });
39958
40564
  }
39959
- function createListCommand3() {
40565
+ function createListCommand4() {
39960
40566
  return new Command("list").description("List an environment's credentials (secrets are never shown)").requiredOption("--env <environmentId>", "Parent environment UUID").option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").option("--no-pager", "Disable the interactive pager for table output").action(async (options) => {
39961
- await runList2(options);
40567
+ await runList3(options);
39962
40568
  });
39963
40569
  }
39964
40570
  function createGetCommand2() {
@@ -39984,18 +40590,19 @@ Examples:
39984
40590
  await runUpdate2(credentialId, options);
39985
40591
  });
39986
40592
  }
39987
- function createDeleteCommand2() {
40593
+ function createDeleteCommand3() {
39988
40594
  return new Command("delete").description("Delete a credential").argument("<credentialId>", "Credential UUID").requiredOption("--env <environmentId>", "Parent environment UUID").option("-y, --yes", "Skip the confirmation prompt (required in non-interactive shells)").action(async (credentialId, options) => {
39989
- await runDelete2(credentialId, options);
40595
+ await runDelete3(credentialId, options);
39990
40596
  });
39991
40597
  }
39992
40598
  function createCredentialCommand() {
39993
40599
  const credential = new Command("credential").alias("cred").description("Manage environment credentials");
39994
- credential.addCommand(createCreateCommand2());
39995
- credential.addCommand(createListCommand3());
40600
+ credential.addCommand(createCreateCommand3());
40601
+ credential.addCommand(createListCommand4());
39996
40602
  credential.addCommand(createGetCommand2());
39997
40603
  credential.addCommand(createUpdateCommand2());
39998
- credential.addCommand(createDeleteCommand2());
40604
+ credential.addCommand(createDeleteCommand3());
40605
+ credential.addCommand(createInboxCommand());
39999
40606
  return credential;
40000
40607
  }
40001
40608
 
@@ -40038,7 +40645,7 @@ function resolveProductionFlag(input) {
40038
40645
  }
40039
40646
  return { value: void 0 };
40040
40647
  }
40041
- function failWriteOp3(action, status, body, notFoundLabel) {
40648
+ function failWriteOp4(action, status, body, notFoundLabel) {
40042
40649
  const { message, errorCode } = readErrorFields(body);
40043
40650
  if (status === UNAUTHORIZED_STATUS2) {
40044
40651
  exitAuthFailed();
@@ -40086,7 +40693,7 @@ function failWriteOp3(action, status, body, notFoundLabel) {
40086
40693
  );
40087
40694
  process.exit(1);
40088
40695
  }
40089
- function failReadOp3(action, status, body, notFoundLabel) {
40696
+ function failReadOp4(action, status, body, notFoundLabel) {
40090
40697
  const { message } = readErrorFields(body);
40091
40698
  if (status === UNAUTHORIZED_STATUS2) {
40092
40699
  exitAuthFailed();
@@ -40227,7 +40834,7 @@ function requireEnvId2(environmentId) {
40227
40834
  process.exit(1);
40228
40835
  }
40229
40836
  }
40230
- async function runCreate3(name, options) {
40837
+ async function runCreate4(name, options) {
40231
40838
  const output = parseOutput(options.output);
40232
40839
  requireAppId(options.app);
40233
40840
  const nameError = validateEnvName(name);
@@ -40249,7 +40856,7 @@ async function runCreate3(name, options) {
40249
40856
  })
40250
40857
  );
40251
40858
  if (result.status !== CREATED_STATUS) {
40252
- failWriteOp3("create", result.status, result.body);
40859
+ failWriteOp4("create", result.status, result.body);
40253
40860
  }
40254
40861
  if (output === "table") {
40255
40862
  console.log(colorSuccess(`Created environment ${sanitizeDisplay(result.body.name)}`));
@@ -40276,7 +40883,7 @@ function createOrgEnvironmentRequests(organizationId) {
40276
40883
  async function collectOrgEnvironments(requests) {
40277
40884
  const appsResult = await requests.listApplications();
40278
40885
  if (!appsResult.ok) {
40279
- failReadOp3("list applications", appsResult.status, appsResult.body);
40886
+ failReadOp4("list applications", appsResult.status, appsResult.body);
40280
40887
  }
40281
40888
  const apps = appsResult.data;
40282
40889
  const environments = [];
@@ -40348,7 +40955,7 @@ async function runListAcrossOrganization(output, pager) {
40348
40955
  disabled: pager === false
40349
40956
  });
40350
40957
  }
40351
- async function runList3(options) {
40958
+ async function runList4(options) {
40352
40959
  const output = parseOutput(options.output);
40353
40960
  if (options.app === void 0) {
40354
40961
  await runListAcrossOrganization(output, options.pager);
@@ -40363,7 +40970,7 @@ async function runList3(options) {
40363
40970
  })
40364
40971
  );
40365
40972
  if (result.status !== SUCCESS_STATUS) {
40366
- failReadOp3("list environments", result.status, result.body, applicationId);
40973
+ failReadOp4("list environments", result.status, result.body, applicationId);
40367
40974
  }
40368
40975
  const envs = result.body;
40369
40976
  if (output === "json") {
@@ -40397,7 +41004,7 @@ async function runGet3(environmentId, options) {
40397
41004
  })
40398
41005
  );
40399
41006
  if (result.status !== SUCCESS_STATUS) {
40400
- failReadOp3("get environment", result.status, result.body, environmentId);
41007
+ failReadOp4("get environment", result.status, result.body, environmentId);
40401
41008
  }
40402
41009
  outputEnvironment(result.body, output);
40403
41010
  }
@@ -40439,14 +41046,14 @@ async function runUpdate3(environmentId, options) {
40439
41046
  })
40440
41047
  );
40441
41048
  if (result.status !== SUCCESS_STATUS) {
40442
- failWriteOp3("update", result.status, result.body, environmentId);
41049
+ failWriteOp4("update", result.status, result.body, environmentId);
40443
41050
  }
40444
41051
  if (output === "table") {
40445
41052
  console.log(colorSuccess(`Updated environment ${sanitizeDisplay(result.body.name)}`));
40446
41053
  }
40447
41054
  outputEnvironment(result.body, output);
40448
41055
  }
40449
- async function runDelete3(environmentId, options) {
41056
+ async function runDelete4(environmentId, options) {
40450
41057
  requireAppId(options.app);
40451
41058
  requireEnvId2(environmentId);
40452
41059
  const confirmed = await confirmDeletion(`environment ${environmentId}`, options.yes);
@@ -40461,11 +41068,11 @@ async function runDelete3(environmentId, options) {
40461
41068
  })
40462
41069
  );
40463
41070
  if (result.status !== NO_CONTENT_STATUS) {
40464
- failWriteOp3("delete", result.status, result.body, environmentId);
41071
+ failWriteOp4("delete", result.status, result.body, environmentId);
40465
41072
  }
40466
41073
  console.log(colorSuccess(`Deleted environment ${environmentId}`));
40467
41074
  }
40468
- function createCreateCommand3() {
41075
+ function createCreateCommand4() {
40469
41076
  return new Command("create").description("Create an environment for an application").argument("<name>", "Environment name (lowercase letters/digits/dashes)").requiredOption("--app <applicationId>", "Parent application UUID").requiredOption("--url <url>", "Primary URL for the environment").option("--description <text>", "Description").option("--production", "Mark as the production environment").option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").addHelpText(
40470
41077
  "after",
40471
41078
  `
@@ -40473,10 +41080,10 @@ Examples:
40473
41080
  tracecli environment create staging --app <appId> --url https://staging.acme.com
40474
41081
  tracecli environment create production --app <appId> --url https://acme.com --production`
40475
41082
  ).action(async (name, options) => {
40476
- await runCreate3(name, options);
41083
+ await runCreate4(name, options);
40477
41084
  });
40478
41085
  }
40479
- function createListCommand4() {
41086
+ function createListCommand5() {
40480
41087
  return new Command("list").description("List environments \u2014 an application's, or every one in the organization").option("--app <applicationId>", "Parent application UUID (omit to list the whole org)").option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").option("--no-pager", "Disable the interactive pager for table output").addHelpText(
40481
41088
  "after",
40482
41089
  `
@@ -40488,7 +41095,7 @@ Examples:
40488
41095
  tracecli environment list --app <appId>
40489
41096
  tracecli environment list --output tsv | cut -f2,3,4`
40490
41097
  ).action(async (options) => {
40491
- await runList3(options);
41098
+ await runList4(options);
40492
41099
  });
40493
41100
  }
40494
41101
  function createGetCommand3() {
@@ -40507,18 +41114,18 @@ Examples:
40507
41114
  await runUpdate3(environmentId, options);
40508
41115
  });
40509
41116
  }
40510
- function createDeleteCommand3() {
41117
+ function createDeleteCommand4() {
40511
41118
  return new Command("delete").description("Delete an environment").argument("<environmentId>", "Environment UUID").requiredOption("--app <applicationId>", "Parent application UUID").option("-y, --yes", "Skip the confirmation prompt (required in non-interactive shells)").action(async (environmentId, options) => {
40512
- await runDelete3(environmentId, options);
41119
+ await runDelete4(environmentId, options);
40513
41120
  });
40514
41121
  }
40515
41122
  function createEnvironmentCommand() {
40516
41123
  const environment = new Command("environment").alias("env").description("Manage application environments");
40517
- environment.addCommand(createCreateCommand3());
40518
- environment.addCommand(createListCommand4());
41124
+ environment.addCommand(createCreateCommand4());
41125
+ environment.addCommand(createListCommand5());
40519
41126
  environment.addCommand(createGetCommand3());
40520
41127
  environment.addCommand(createUpdateCommand3());
40521
- environment.addCommand(createDeleteCommand3());
41128
+ environment.addCommand(createDeleteCommand4());
40522
41129
  return environment;
40523
41130
  }
40524
41131
 
@@ -40550,7 +41157,223 @@ var CLI_PACKAGE_NAME = packageMetadata.name;
40550
41157
  var CLI_VERSION = packageMetadata.version;
40551
41158
 
40552
41159
  // src/pentest/commands.ts
40553
- import { readFileSync as readFileSync4 } from "fs";
41160
+ import { readFileSync as readFileSync5 } from "fs";
41161
+
41162
+ // src/pentest/evidence.ts
41163
+ import { readFileSync as readFileSync4, statSync } from "fs";
41164
+ import { basename } from "path";
41165
+
41166
+ // src/pentest/resolve.ts
41167
+ var UUID_PATTERN2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
41168
+ async function resolvePentestId(organizationId, identifier) {
41169
+ if (UUID_PATTERN2.test(identifier)) {
41170
+ return identifier;
41171
+ }
41172
+ const numericMatch = identifier.match(/^(?:PT-)?(\d+)$/i);
41173
+ if (!numericMatch?.[1]) {
41174
+ console.error(
41175
+ `Invalid pentest identifier '${identifier}'. Use a display ID (e.g. PT-3) or UUID.`
41176
+ );
41177
+ process.exit(1);
41178
+ }
41179
+ const displayId = Number.parseInt(numericMatch[1], 10);
41180
+ const result = await authenticatedRequest(
41181
+ (client) => client.pentest.listPentests({
41182
+ params: { organizationId }
41183
+ })
41184
+ );
41185
+ if (result.status === 401) {
41186
+ console.error("Authentication failed. Run `tracecli auth login` to re-authenticate.");
41187
+ process.exit(1);
41188
+ }
41189
+ if (result.status !== 200) {
41190
+ console.error(`Failed to list pentests (status ${result.status}).`);
41191
+ process.exit(1);
41192
+ }
41193
+ const pentest = result.body.find((p2) => p2.displayId === displayId);
41194
+ if (!pentest) {
41195
+ console.error(
41196
+ `Pentest with display ID ${displayId} not found.
41197
+ Run \`tracecli pentest list\` to see available pentests.`
41198
+ );
41199
+ process.exit(1);
41200
+ }
41201
+ return pentest.pentestId;
41202
+ }
41203
+
41204
+ // src/pentest/evidence.ts
41205
+ var SECTION_LIST = ValidationEvidenceSectionValues.join(", ");
41206
+ var KIND_LIST = PentestExploitArtifactKindValues.join(", ");
41207
+ function isSection(value) {
41208
+ return ValidationEvidenceSectionValues.includes(value);
41209
+ }
41210
+ function isKind(value) {
41211
+ return PentestExploitArtifactKindValues.includes(value);
41212
+ }
41213
+ function formatBytes(bytes) {
41214
+ if (bytes < 1024) return `${bytes} B`;
41215
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
41216
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
41217
+ }
41218
+ async function resolveFindingId(organizationId, pentestId, identifier) {
41219
+ if (isUuid(identifier)) {
41220
+ return identifier;
41221
+ }
41222
+ const numericMatch = identifier.match(/^(?:PTF-)?(\d+)$/i);
41223
+ if (!numericMatch?.[1]) {
41224
+ console.error(
41225
+ colorError(
41226
+ `Invalid finding identifier '${sanitizeDisplay(identifier)}'. Use a finding number (e.g. PTF-7) or UUID.`
41227
+ )
41228
+ );
41229
+ process.exit(1);
41230
+ }
41231
+ const displayId = Number.parseInt(numericMatch[1], 10);
41232
+ const result = await authenticatedRequest(
41233
+ (client) => client.pentestFinding.listFindings({ params: { organizationId, pentestId } })
41234
+ );
41235
+ if (result.status === UNAUTHORIZED_STATUS2) {
41236
+ exitAuthFailed();
41237
+ }
41238
+ if (result.status !== 200) {
41239
+ console.error(colorError(`Failed to list findings (status ${result.status}).`));
41240
+ process.exit(1);
41241
+ }
41242
+ const match = result.body.find((f) => f.displayId === displayId);
41243
+ if (!match || !("pentestFindingId" in match)) {
41244
+ console.error(
41245
+ colorError(
41246
+ `No finding PTF-${displayId} in this pentest. Unpublished findings have no number yet \u2014 pass the finding UUID instead (\`tracecli pentest findings list --pentest <id> --output json\`).`
41247
+ )
41248
+ );
41249
+ process.exit(1);
41250
+ }
41251
+ return match.pentestFindingId;
41252
+ }
41253
+ async function runAttachEvidence(options) {
41254
+ if (options.section !== void 0 && !isSection(options.section)) {
41255
+ console.error(colorError(`Invalid --section '${sanitizeDisplay(options.section)}'.`));
41256
+ console.error(`Valid sections: ${SECTION_LIST}`);
41257
+ process.exit(1);
41258
+ }
41259
+ if (options.kind !== void 0 && !isKind(options.kind)) {
41260
+ console.error(colorError(`Invalid --kind '${sanitizeDisplay(options.kind)}'.`));
41261
+ console.error(`Valid kinds: ${KIND_LIST}`);
41262
+ process.exit(1);
41263
+ }
41264
+ let sizeBytes;
41265
+ try {
41266
+ const stats = statSync(options.file);
41267
+ if (!stats.isFile()) {
41268
+ console.error(colorError(`--file '${sanitizeDisplay(options.file)}' is not a regular file.`));
41269
+ process.exit(1);
41270
+ }
41271
+ sizeBytes = stats.size;
41272
+ } catch {
41273
+ console.error(colorError(`Cannot read --file '${sanitizeDisplay(options.file)}'.`));
41274
+ process.exit(1);
41275
+ }
41276
+ if (sizeBytes === 0) {
41277
+ console.error(colorError(`--file '${sanitizeDisplay(options.file)}' is empty.`));
41278
+ process.exit(1);
41279
+ }
41280
+ if (sizeBytes > MAX_STAFF_EVIDENCE_UPLOAD_BYTES) {
41281
+ console.error(
41282
+ colorError(
41283
+ `File is ${formatBytes(sizeBytes)}; the limit for one exhibit is ${formatBytes(MAX_STAFF_EVIDENCE_UPLOAD_BYTES)}.`
41284
+ )
41285
+ );
41286
+ process.exit(1);
41287
+ }
41288
+ const organizationId = await resolveOrganizationId();
41289
+ const pentestId = await resolvePentestId(organizationId, options.pentest);
41290
+ const pentestFindingId = await resolveFindingId(organizationId, pentestId, options.finding);
41291
+ const params = { organizationId, pentestId, pentestFindingId };
41292
+ const filename = basename(options.file);
41293
+ const slot = await authenticatedRequest(
41294
+ (client) => client.pentestFinding.createFindingEvidenceUpload({
41295
+ params,
41296
+ body: { filename, sizeBytes }
41297
+ })
41298
+ );
41299
+ if (slot.status === UNAUTHORIZED_STATUS2) {
41300
+ exitAuthFailed();
41301
+ }
41302
+ if (slot.status !== 201) {
41303
+ const { message } = readErrorFields(slot.body);
41304
+ console.error(
41305
+ colorError(message ?? `Failed to start the evidence upload (status ${slot.status}).`)
41306
+ );
41307
+ process.exit(1);
41308
+ }
41309
+ const body = readFileSync4(options.file);
41310
+ const put = await fetch(slot.body.uploadUrl, { method: "PUT", body: new Uint8Array(body) });
41311
+ if (!put.ok) {
41312
+ console.error(
41313
+ colorError(
41314
+ `Upload to storage failed (status ${put.status}). The upload link is short-lived \u2014 re-run the command.`
41315
+ )
41316
+ );
41317
+ process.exit(1);
41318
+ }
41319
+ const attached = await authenticatedRequest(
41320
+ (client) => client.pentestFinding.attachFindingEvidence({
41321
+ params,
41322
+ body: {
41323
+ key: slot.body.key,
41324
+ ...options.caption !== void 0 ? { caption: options.caption } : {},
41325
+ ...options.section !== void 0 && isSection(options.section) ? { section: options.section } : {},
41326
+ ...options.kind !== void 0 && isKind(options.kind) ? { kind: options.kind } : {},
41327
+ ...options.customerFacing ? { customerFacing: true } : {}
41328
+ }
41329
+ })
41330
+ );
41331
+ if (attached.status === UNAUTHORIZED_STATUS2) {
41332
+ exitAuthFailed();
41333
+ }
41334
+ if (attached.status !== 201) {
41335
+ const { message } = readErrorFields(attached.body);
41336
+ console.error(
41337
+ colorError(message ?? `Failed to attach the evidence (status ${attached.status}).`)
41338
+ );
41339
+ process.exit(1);
41340
+ }
41341
+ if (options.output === "json") {
41342
+ console.log(JSON.stringify(attached.body, null, 2));
41343
+ return;
41344
+ }
41345
+ const visibility = attached.body.customerFacing ? "customer-facing" : "staff-internal";
41346
+ const placement = attached.body.section ? ` \xB7 ${attached.body.section}` : "";
41347
+ console.log(
41348
+ colorSuccess(
41349
+ `Attached ${attached.body.kind} \xB7 ${formatBytes(attached.body.sizeBytes)} \xB7 ${visibility}${placement}`
41350
+ )
41351
+ );
41352
+ console.log(` artifact ${attached.body.artifactId}`);
41353
+ if (attached.body.kind === "HAR") {
41354
+ console.log(" HAR captures stay internal \u2014 they can carry a live session token.");
41355
+ } else if (!attached.body.customerFacing) {
41356
+ console.log(" Publish it from the finding\u2019s evidence panel in the staff console.");
41357
+ }
41358
+ }
41359
+ function createAttachEvidenceCommand() {
41360
+ return new Command("attach-evidence").description("Attach a screenshot, recording, or capture to a pentest finding (staff)").requiredOption("--pentest <id>", "Pentest display ID (e.g. PT-3) or UUID").requiredOption("--finding <id>", "Finding number (e.g. PTF-7) or UUID").requiredOption("--file <path>", "Path to the evidence file").option("--caption <text>", "What the exhibit shows; rendered as its label").option("--section <section>", `Report section it backs (${SECTION_LIST})`).option("--kind <kind>", `Override the derived exhibit kind (${KIND_LIST})`).option("--customer-facing", "Publish to the customer (not allowed for HAR or video)").option("-o, --output <format>", "Output format: text, json (default: text)", "text").addHelpText(
41361
+ "after",
41362
+ `
41363
+ The file uploads straight to storage, so recordings of any reasonable size work.
41364
+ Videos and HAR captures always land staff-internal \u2014 publish a video with the
41365
+ toggle in the staff console's evidence panel after reviewing it for redaction.
41366
+
41367
+ Examples:
41368
+ tracecli pentest findings attach-evidence --pentest PT-3 --finding 7 --file ./poc.png
41369
+ tracecli pentest findings attach-evidence --pentest PT-3 --finding PTF-7 --file ./poc.mov \\
41370
+ --caption "Cookie survives logout, replayed 40s later" --section proofOfConcept
41371
+ tracecli pentest findings attach-evidence --pentest PT-3 --finding 7 --file ./roster.json \\
41372
+ --caption "Full member roster returned to a non-member" --customer-facing --output json`
41373
+ ).action(async (options) => {
41374
+ await runAttachEvidence(options);
41375
+ });
41376
+ }
40554
41377
 
40555
41378
  // src/pentest/questionnaire.ts
40556
41379
  function buildUpsertQuestionnaireBody(answers) {
@@ -40633,9 +41456,9 @@ function formatAnswerValue(key, answers) {
40633
41456
  }
40634
41457
  }
40635
41458
  function formatQuestionnaireShow(input) {
40636
- const lines = [
40637
- `Editable: ${input.editable ? "yes (scoping window open)" : "no (editing closed after scoping)"}`
40638
- ];
41459
+ const submitted = input.submitted === true;
41460
+ const editableLine = input.editable ? "yes (scoping window open)" : submitted ? "no (submitted)" : "no (editing closed after scoping)";
41461
+ const lines = [`Editable: ${editableLine}`, `Submitted: ${submitted ? "yes" : "no"}`];
40639
41462
  if (input.answers === null || Object.keys(input.answers).length === 0) {
40640
41463
  lines.push("Answers: (none)");
40641
41464
  return lines.join("\n");
@@ -40695,43 +41518,7 @@ function formatFindingSeverity(severity) {
40695
41518
  return severity;
40696
41519
  }
40697
41520
  }
40698
- var UUID_PATTERN2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
40699
- async function resolvePentestId(organizationId, identifier) {
40700
- if (UUID_PATTERN2.test(identifier)) {
40701
- return identifier;
40702
- }
40703
- const numericMatch = identifier.match(/^(?:PT-)?(\d+)$/i);
40704
- if (!numericMatch?.[1]) {
40705
- console.error(
40706
- `Invalid pentest identifier '${identifier}'. Use a display ID (e.g. PT-3) or UUID.`
40707
- );
40708
- process.exit(1);
40709
- }
40710
- const displayId = Number.parseInt(numericMatch[1], 10);
40711
- const result = await authenticatedRequest(
40712
- (client) => client.pentest.listPentests({
40713
- params: { organizationId }
40714
- })
40715
- );
40716
- if (result.status === 401) {
40717
- console.error("Authentication failed. Run `tracecli auth login` to re-authenticate.");
40718
- process.exit(1);
40719
- }
40720
- if (result.status !== 200) {
40721
- console.error(`Failed to list pentests (status ${result.status}).`);
40722
- process.exit(1);
40723
- }
40724
- const pentest = result.body.find((p2) => p2.displayId === displayId);
40725
- if (!pentest) {
40726
- console.error(
40727
- `Pentest with display ID ${displayId} not found.
40728
- Run \`tracecli pentest list\` to see available pentests.`
40729
- );
40730
- process.exit(1);
40731
- }
40732
- return pentest.pentestId;
40733
- }
40734
- function createListCommand5() {
41521
+ function createListCommand6() {
40735
41522
  return new Command("list").description("List pentests for your organization").option(
40736
41523
  "--status <status>",
40737
41524
  "Filter by status (SCOPING, IN_PROGRESS, REMEDIATION, CLOSED)",
@@ -40981,6 +41768,7 @@ function renderPentestFindingLines(finding) {
40981
41768
  function createFindingsCommand() {
40982
41769
  const findings = new Command("findings").description("Manage pentest findings");
40983
41770
  findings.addCommand(createFindingsListCommand());
41771
+ findings.addCommand(createAttachEvidenceCommand());
40984
41772
  return findings;
40985
41773
  }
40986
41774
  var PRIORITY_SET = new Set(CustomerPriorityCategoryValues);
@@ -41251,7 +42039,7 @@ async function fetchQuestionnaire(organizationId, pentestId, notFoundLabel) {
41251
42039
  if (result.status !== SUCCESS_STATUS) {
41252
42040
  failQuestionnaire(result.status, result.body, "read", notFoundLabel);
41253
42041
  }
41254
- return result.body;
42042
+ return toQuestionnaireGetBody(result.body);
41255
42043
  }
41256
42044
  async function putQuestionnaire(organizationId, pentestId, answers, notFoundLabel) {
41257
42045
  const body = buildUpsertQuestionnaireBody(answers);
@@ -41264,7 +42052,35 @@ async function putQuestionnaire(organizationId, pentestId, answers, notFoundLabe
41264
42052
  if (result.status !== SUCCESS_STATUS) {
41265
42053
  failQuestionnaire(result.status, result.body, "write", notFoundLabel);
41266
42054
  }
41267
- return result.body;
42055
+ return toQuestionnaireGetBody(result.body);
42056
+ }
42057
+ async function submitQuestionnaire(organizationId, pentestId, answers, notFoundLabel) {
42058
+ const result = await authenticatedRequest(
42059
+ (client) => client.pentest.submitCustomerPentestQuestionnaire({
42060
+ params: { organizationId, pentestId },
42061
+ body: answers === void 0 ? {} : { answers }
42062
+ })
42063
+ );
42064
+ if (result.status !== SUCCESS_STATUS) {
42065
+ failQuestionnaire(result.status, result.body, "write", notFoundLabel);
42066
+ }
42067
+ return toQuestionnaireGetBody(result.body);
42068
+ }
42069
+ function toQuestionnaireGetBody(body) {
42070
+ return {
42071
+ pentestId: body.pentestId,
42072
+ editable: body.editable,
42073
+ answers: body.answers,
42074
+ updatedAt: body.updatedAt,
42075
+ ...body.submittedAt !== void 0 ? { submittedAt: body.submittedAt } : {}
42076
+ };
42077
+ }
42078
+ function questionnaireShowInput(body) {
42079
+ return {
42080
+ answers: body.answers,
42081
+ editable: body.editable,
42082
+ submitted: body.submittedAt != null
42083
+ };
41268
42084
  }
41269
42085
  function exitCancelled() {
41270
42086
  me("Cancelled.");
@@ -41330,15 +42146,20 @@ async function promptQuestionnaireAnswers(existing) {
41330
42146
  async function runQuestionnaire(pentestIdentifier, options) {
41331
42147
  const show = options.show === true;
41332
42148
  const filePath = options.file;
42149
+ const submit = options.submit === true;
41333
42150
  if (show && filePath !== void 0) {
41334
42151
  console.error(colorError("Pass only one of --show / --file."));
41335
42152
  process.exit(1);
41336
42153
  }
42154
+ if (show && submit) {
42155
+ console.error(colorError("Pass only one of --show / --submit."));
42156
+ process.exit(1);
42157
+ }
41337
42158
  const isInteractiveTty = Boolean(process.stdin.isTTY && process.stdout.isTTY);
41338
- if (!show && filePath === void 0 && !isInteractiveTty) {
42159
+ if (!show && filePath === void 0 && !submit && !isInteractiveTty) {
41339
42160
  console.error(
41340
42161
  colorError(
41341
- "Interactive questionnaire requires a TTY. Use --show to print answers, or --file <path> to upload an answers JSON object."
42162
+ "Interactive questionnaire requires a TTY. Use --show to print answers, --file <path> to upload an answers JSON object, or --submit to freeze the current answers."
41342
42163
  )
41343
42164
  );
41344
42165
  process.exit(2);
@@ -41347,18 +42168,13 @@ async function runQuestionnaire(pentestIdentifier, options) {
41347
42168
  const pentestId = await resolvePentestId(organizationId, pentestIdentifier);
41348
42169
  if (show) {
41349
42170
  const current2 = await fetchQuestionnaire(organizationId, pentestId, pentestIdentifier);
41350
- console.log(
41351
- formatQuestionnaireShow({
41352
- answers: current2.answers,
41353
- editable: current2.editable
41354
- })
41355
- );
42171
+ console.log(formatQuestionnaireShow(questionnaireShowInput(current2)));
41356
42172
  return;
41357
42173
  }
41358
42174
  if (filePath !== void 0) {
41359
42175
  let raw;
41360
42176
  try {
41361
- raw = readFileSync4(filePath, "utf8");
42177
+ raw = readFileSync5(filePath, "utf8");
41362
42178
  } catch {
41363
42179
  console.error(colorError(`Could not read answers file: ${filePath}`));
41364
42180
  process.exit(1);
@@ -41368,26 +42184,31 @@ async function runQuestionnaire(pentestIdentifier, options) {
41368
42184
  console.error(colorError(parsed.error));
41369
42185
  process.exit(1);
41370
42186
  }
41371
- const saved2 = await putQuestionnaire(
42187
+ const saved2 = submit ? await submitQuestionnaire(organizationId, pentestId, parsed.answers, pentestIdentifier) : await putQuestionnaire(organizationId, pentestId, parsed.answers, pentestIdentifier);
42188
+ console.log(
42189
+ colorSuccess(
42190
+ submit ? "Submitted questionnaire. Answers are locked." : "Saved questionnaire answers."
42191
+ )
42192
+ );
42193
+ console.log(formatQuestionnaireShow(questionnaireShowInput(saved2)));
42194
+ return;
42195
+ }
42196
+ if (submit) {
42197
+ const submitted = await submitQuestionnaire(
41372
42198
  organizationId,
41373
42199
  pentestId,
41374
- parsed.answers,
42200
+ void 0,
41375
42201
  pentestIdentifier
41376
42202
  );
41377
- console.log(colorSuccess("Saved questionnaire answers."));
41378
- console.log(
41379
- formatQuestionnaireShow({
41380
- answers: saved2.answers,
41381
- editable: saved2.editable
41382
- })
41383
- );
42203
+ console.log(colorSuccess("Submitted questionnaire. Answers are locked."));
42204
+ console.log(formatQuestionnaireShow(questionnaireShowInput(submitted)));
41384
42205
  return;
41385
42206
  }
41386
42207
  const current = await fetchQuestionnaire(organizationId, pentestId, pentestIdentifier);
41387
42208
  if (!current.editable) {
41388
42209
  console.error(
41389
42210
  colorError(
41390
- "Questionnaire editing is closed after scoping starts. Use --show to view the saved answers."
42211
+ current.submittedAt != null ? "Questionnaire has already been submitted. Use --show to view the saved answers." : "Questionnaire editing is closed after scoping starts. Use --show to view the saved answers."
41391
42212
  )
41392
42213
  );
41393
42214
  process.exit(1);
@@ -41395,35 +42216,33 @@ async function runQuestionnaire(pentestIdentifier, options) {
41395
42216
  const merged = await promptQuestionnaireAnswers(current.answers);
41396
42217
  const saved = await putQuestionnaire(organizationId, pentestId, merged, pentestIdentifier);
41397
42218
  console.log(colorSuccess("Saved questionnaire answers."));
41398
- console.log(
41399
- formatQuestionnaireShow({
41400
- answers: saved.answers,
41401
- editable: saved.editable
41402
- })
41403
- );
42219
+ console.log(formatQuestionnaireShow(questionnaireShowInput(saved)));
41404
42220
  }
41405
42221
  function createQuestionnaireCommand() {
41406
42222
  return new Command("questionnaire").description("View or fill the customer pentest intake questionnaire").argument("<pentestId>", "Pentest display ID (e.g. PT-3) or UUID").option("--show", "Print current answers using shared labels (GET)").option(
41407
42223
  "--file <path>",
41408
42224
  "Upload answers from a JSON file (answers object only \u2014 not { answers }; whole-object replace)"
41409
- ).addHelpText(
42225
+ ).option("--submit", "Freeze the questionnaire after saving (or freeze the current answers)").addHelpText(
41410
42226
  "after",
41411
42227
  `
41412
42228
  Optional scoping questionnaire for a new pentest (editable while stage is DRAFT
41413
- or READY_TO_START). Reads accept a browser session or API key; writes require an
41414
- organization admin (or Trace staff) signed in via \`tracecli auth login\` \u2014 API
41415
- keys are rejected.
42229
+ or READY_TO_START, until submitted). Reads accept a browser session or API key;
42230
+ writes require an organization admin (or Trace staff) signed in via \`tracecli
42231
+ auth login\` \u2014 API keys are rejected.
41416
42232
 
41417
42233
  Modes:
41418
42234
  --show GET and print key: value with shared customer-facing labels
41419
42235
  --file <path> PUT the file's answers object (CLI wraps { answers } for the API)
42236
+ --submit Freeze answers (combine with --file or TTY fill, or alone to freeze current)
41420
42237
  (TTY default) load-merge-write: GET, prompt each question, confirm, PUT
41421
42238
 
41422
- Non-interactive shells must pass --show or --file (exit 2 otherwise).
42239
+ Non-interactive shells must pass --show, --file, or --submit (exit 2 otherwise).
41423
42240
 
41424
42241
  Examples:
41425
42242
  tracecli pentest questionnaire PT-1 --show
41426
42243
  tracecli pentest questionnaire PT-1 --file answers.json
42244
+ tracecli pentest questionnaire PT-1 --file answers.json --submit
42245
+ tracecli pentest questionnaire PT-1 --submit
41427
42246
  tracecli pentest questionnaire PT-1`
41428
42247
  ).action(async (pentestId, options) => {
41429
42248
  await runQuestionnaire(pentestId, options);
@@ -41431,7 +42250,7 @@ Examples:
41431
42250
  }
41432
42251
  function createPentestCommand() {
41433
42252
  const pentest = new Command("pentest").description("Manage pentests");
41434
- pentest.addCommand(createListCommand5());
42253
+ pentest.addCommand(createListCommand6());
41435
42254
  pentest.addCommand(createUpdateCommand4());
41436
42255
  pentest.addCommand(createQuestionnaireCommand());
41437
42256
  pentest.addCommand(createFindingsCommand());
@@ -41439,7 +42258,7 @@ function createPentestCommand() {
41439
42258
  }
41440
42259
 
41441
42260
  // src/update/commands.ts
41442
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
42261
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
41443
42262
  import { dirname as dirname2, join as join5 } from "path";
41444
42263
  import { createInterface as createInterface4 } from "readline/promises";
41445
42264
  var UPDATE_CACHE_FILE = "update-check.json";
@@ -41457,7 +42276,7 @@ function readUpdateCheckCache() {
41457
42276
  return null;
41458
42277
  }
41459
42278
  try {
41460
- const rawCache = readFileSync5(cachePath, "utf-8");
42279
+ const rawCache = readFileSync6(cachePath, "utf-8");
41461
42280
  const parsed = JSON.parse(rawCache);
41462
42281
  if (typeof parsed !== "object" || parsed === null) {
41463
42282
  return null;
@@ -42056,7 +42875,7 @@ Run \`tracecli vulnerabilities list\` to see available vulnerabilities.`
42056
42875
  printVulnerabilityDetail(result.body);
42057
42876
  });
42058
42877
  }
42059
- function createListCommand6() {
42878
+ function createListCommand7() {
42060
42879
  return new Command("list").description("List vulnerabilities for your organization").option("--repo <repo>", "Filter by repository name or owner/repo").option(
42061
42880
  "--severity <severity>",
42062
42881
  "Filter by severity (CRITICAL, HIGH, MEDIUM, LOW)",
@@ -42190,7 +43009,7 @@ Examples:
42190
43009
  function createVulnerabilitiesCommand() {
42191
43010
  const vulnerabilities = new Command("vulnerabilities").description("Manage vulnerabilities");
42192
43011
  vulnerabilities.addCommand(createGetCommand4());
42193
- vulnerabilities.addCommand(createListCommand6());
43012
+ vulnerabilities.addCommand(createListCommand7());
42194
43013
  return vulnerabilities;
42195
43014
  }
42196
43015
 
@@ -42211,6 +43030,7 @@ program2.addCommand(createEnvironmentCommand());
42211
43030
  program2.addCommand(createCredentialCommand());
42212
43031
  program2.addCommand(createRepoCommand());
42213
43032
  program2.addCommand(createVulnerabilitiesCommand());
43033
+ program2.addCommand(createCodeSizeCommand());
42214
43034
  program2.addCommand(createPentestCommand());
42215
43035
  program2.addCommand(createCompletionCommand(program2));
42216
43036
  program2.addCommand(createUpgradeCommand());