securewithtrace 0.42.0 → 0.43.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 +11 -0
  2. package/dist/index.js +252 -111
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -263,6 +263,9 @@ tracecli pentest findings list --pentest PT-3 --output tsv | cut -f1,4
263
263
  # Open the pentest detail page in a browser
264
264
  tracecli pentest findings list --pentest PT-3 --web
265
265
 
266
+ # One finding with its full write-up, as markdown for a coding agent
267
+ tracecli pentest findings get --pentest PT-3 --finding PTF-7
268
+
266
269
  # Mark a finding in progress, then remediated when the fix is ready for retest
267
270
  tracecli pentest findings status-update --pentest PT-3 --finding 7 --status IN_PROGRESS
268
271
  tracecli pentest findings status-update --pentest PT-3 --finding 7 --status REMEDIATED
@@ -284,6 +287,14 @@ Flags for `pentest findings list`:
284
287
  | `--output <format>` | `-o` | Output format: `table` (default), `json`, `tsv` |
285
288
  | `--web` | `-w` | Open the pentest detail page in a browser |
286
289
 
290
+ Flags for `pentest findings get`:
291
+
292
+ | Flag | Shorthand | Description |
293
+ |------|-----------|-------------|
294
+ | `--pentest <id>` | | Pentest display ID (e.g. `PT-3`) or UUID (required) |
295
+ | `--finding <id>` | | Finding number (e.g. `PTF-7`) or UUID (required) |
296
+ | `--output <format>` | `-o` | Output format: `text` (markdown, default), `json` |
297
+
287
298
  Flags for `pentest findings status-update`:
288
299
 
289
300
  | Flag | Shorthand | Description |
package/dist/index.js CHANGED
@@ -46485,117 +46485,6 @@ Examples:
46485
46485
  });
46486
46486
  }
46487
46487
 
46488
- // src/pentest/questionnaire.ts
46489
- function buildUpsertQuestionnaireBody(answers) {
46490
- if (answers === null) {
46491
- return { answers: null };
46492
- }
46493
- return { answers: normalizePentestCustomerIntakeAnswers(answers) };
46494
- }
46495
- function parseQuestionnaireAnswersFile(raw, sourceLabel = "file") {
46496
- let parsed;
46497
- try {
46498
- parsed = JSON.parse(raw);
46499
- } catch {
46500
- return { error: `Invalid JSON in ${sourceLabel}.` };
46501
- }
46502
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
46503
- return {
46504
- error: `${sourceLabel} must be a JSON object of questionnaire answers (not an array or null).`
46505
- };
46506
- }
46507
- if ("answers" in parsed && Object.keys(parsed).length === 1) {
46508
- return {
46509
- error: `${sourceLabel} must contain the answers object directly, not an { answers } wrapper. The CLI wraps it for the API.`
46510
- };
46511
- }
46512
- const result = PentestCustomerIntakeAnswersSchema.safeParse(parsed);
46513
- if (!result.success) {
46514
- const issue2 = result.error.issues[0];
46515
- const path = issue2?.path.length ? issue2.path.join(".") : "answers";
46516
- const message = issue2?.message ?? "invalid";
46517
- return { error: `Invalid questionnaire answers in ${sourceLabel}: ${path} \u2014 ${message}` };
46518
- }
46519
- return { answers: normalizePentestCustomerIntakeAnswers(result.data) };
46520
- }
46521
- function applyTextIntakeAnswer(current, key, input) {
46522
- const trimmed = input.trim();
46523
- if (trimmed.length === 0) {
46524
- return current;
46525
- }
46526
- return { ...current, [key]: trimmed };
46527
- }
46528
- function applyMultiSelectIntakeAnswer(current, key, selected) {
46529
- if (selected.length === 0) {
46530
- return current;
46531
- }
46532
- return {
46533
- ...current,
46534
- [key]: Array.from(new Set(selected)).sort()
46535
- };
46536
- }
46537
- function formatIssueClasses(values) {
46538
- return values.map((v) => INTAKE_ISSUE_CLASS_LABELS[v]).join(", ");
46539
- }
46540
- function formatSurfaces(values) {
46541
- return values.map((v) => INTAKE_SURFACE_LABELS[v]).join(", ");
46542
- }
46543
- function formatAnswerValue(key, answers) {
46544
- switch (key) {
46545
- case "issueClasses": {
46546
- const values = answers.issueClasses;
46547
- if (values === void 0 || values.length === 0) {
46548
- return null;
46549
- }
46550
- return formatIssueClasses(values);
46551
- }
46552
- case "surfaces": {
46553
- const values = answers.surfaces;
46554
- if (values === void 0 || values.length === 0) {
46555
- return null;
46556
- }
46557
- return formatSurfaces(values);
46558
- }
46559
- default: {
46560
- const value = answers[key];
46561
- if (value === void 0 || value.trim().length === 0) {
46562
- return null;
46563
- }
46564
- return value;
46565
- }
46566
- }
46567
- }
46568
- function formatQuestionnaireShow(input) {
46569
- const submitted = input.submitted === true;
46570
- const editableLine = input.editable ? "yes (scoping window open)" : submitted ? "no (submitted)" : "no (editing closed after scoping)";
46571
- const lines = [`Editable: ${editableLine}`, `Submitted: ${submitted ? "yes" : "no"}`];
46572
- if (input.answers === null || Object.keys(input.answers).length === 0) {
46573
- lines.push("Answers: (none)");
46574
- return lines.join("\n");
46575
- }
46576
- lines.push("Answers:");
46577
- for (const key of PENTEST_CUSTOMER_INTAKE_ANSWER_KEYS) {
46578
- const value = formatAnswerValue(key, input.answers);
46579
- if (value === null) {
46580
- continue;
46581
- }
46582
- lines.push(`${PENTEST_CUSTOMER_INTAKE_LABELS[key]}: ${value}`);
46583
- }
46584
- return lines.join("\n");
46585
- }
46586
- function issueClassSelectOptions() {
46587
- return CustomerPriorityCategoryValues.map((value) => ({
46588
- value,
46589
- label: INTAKE_ISSUE_CLASS_LABELS[value]
46590
- }));
46591
- }
46592
- function surfaceSelectOptions() {
46593
- return IntakeSurfaceValues.map((value) => ({
46594
- value,
46595
- label: INTAKE_SURFACE_LABELS[value]
46596
- }));
46597
- }
46598
-
46599
46488
  // src/pentest/status-update.ts
46600
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;
46601
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;
@@ -46777,6 +46666,257 @@ Examples:
46777
46666
  });
46778
46667
  }
46779
46668
 
46669
+ // src/pentest/get-finding.ts
46670
+ var OUTPUT_FORMATS2 = ["text", "json"];
46671
+ function isOutputFormat2(value) {
46672
+ return OUTPUT_FORMATS2.includes(value);
46673
+ }
46674
+ var PROSE_SECTIONS = [
46675
+ { key: "description", heading: "Description" },
46676
+ { key: "impact", heading: "Impact" },
46677
+ { key: "attackScenario", heading: "Attack Scenario" },
46678
+ { key: "proofOfConcept", heading: "Proof of Concept" },
46679
+ { key: "rootCauseAnalysis", heading: "Root Cause Analysis" },
46680
+ { key: "remediation", heading: "Remediation" },
46681
+ { key: "severityConsiderations", heading: "Severity Considerations" },
46682
+ { key: "verification", heading: "Verification" }
46683
+ ];
46684
+ function sanitizeMarkdown(value) {
46685
+ return value.split("\n").map(sanitizeDisplay).join("\n");
46686
+ }
46687
+ function formatFindingMarkdown(finding) {
46688
+ const lines = [];
46689
+ lines.push(`# PTF-${finding.displayId}: ${sanitizeDisplay(finding.title)}`);
46690
+ lines.push("");
46691
+ lines.push(`- Severity: ${sanitizeDisplay(finding.severity)}`);
46692
+ lines.push(`- Status: ${sanitizeDisplay(finding.status)}`);
46693
+ lines.push(`- Category: ${sanitizeDisplay(finding.category)}`);
46694
+ if (finding.cweIds.length > 0) {
46695
+ lines.push(`- CWE: ${finding.cweIds.map(sanitizeDisplay).join(", ")}`);
46696
+ }
46697
+ if (finding.owaspCategory) {
46698
+ lines.push(`- OWASP: ${sanitizeDisplay(finding.owaspCategory)}`);
46699
+ }
46700
+ if (finding.cvssScore !== null || finding.cvssVector) {
46701
+ const parts = [
46702
+ finding.cvssScore !== null ? String(finding.cvssScore) : null,
46703
+ finding.cvssVector === null ? null : sanitizeDisplay(finding.cvssVector)
46704
+ ].filter((part) => part !== null);
46705
+ lines.push(`- CVSS: ${parts.join(" ")}`);
46706
+ }
46707
+ if (finding.repositories.length > 0) {
46708
+ lines.push("");
46709
+ lines.push("## Affected Code");
46710
+ lines.push("");
46711
+ for (const repository of finding.repositories) {
46712
+ const repositoryName = sanitizeDisplay(repository.repositoryName);
46713
+ const filePath = repository.filePath === null ? null : sanitizeDisplay(repository.filePath);
46714
+ const location = filePath === null ? repositoryName : `${repositoryName}: ${filePath}${repository.startLine === null ? "" : `:${repository.startLine}${repository.endLine !== null && repository.endLine !== repository.startLine ? `-${repository.endLine}` : ""}`}`;
46715
+ const commitSha = repository.commitSha === null ? null : sanitizeDisplay(repository.commitSha).slice(0, 12);
46716
+ lines.push(`- ${location}${commitSha ? ` (commit ${commitSha})` : ""}`);
46717
+ }
46718
+ }
46719
+ for (const section of PROSE_SECTIONS) {
46720
+ const body = finding[section.key];
46721
+ if (!body) continue;
46722
+ lines.push("");
46723
+ lines.push(`## ${section.heading}`);
46724
+ lines.push("");
46725
+ lines.push(sanitizeMarkdown(body.trim()));
46726
+ }
46727
+ lines.push("");
46728
+ return lines.join("\n");
46729
+ }
46730
+ function failGetFinding(status, body) {
46731
+ const { message } = readErrorFields(body);
46732
+ if (status === UNAUTHORIZED_STATUS2) {
46733
+ exitAuthFailed();
46734
+ }
46735
+ if (status === 404) {
46736
+ console.error(colorError(message ?? "Pentest or finding not found."));
46737
+ process.exit(1);
46738
+ }
46739
+ if (status === 403) {
46740
+ console.error(colorError(message ?? "You do not have access to this finding (status 403)."));
46741
+ process.exit(1);
46742
+ }
46743
+ console.error(
46744
+ colorError(`Failed to fetch finding (status ${status})${message ? `: ${message}` : "."}`)
46745
+ );
46746
+ process.exit(1);
46747
+ }
46748
+ async function runGetFinding(options) {
46749
+ if (!isOutputFormat2(options.output)) {
46750
+ console.error(
46751
+ colorError(`Invalid --output '${sanitizeDisplay(options.output)}'. Use text or json.`)
46752
+ );
46753
+ process.exit(1);
46754
+ }
46755
+ if (!isPentestIdentifier(options.pentest)) {
46756
+ console.error(
46757
+ colorError(
46758
+ `Invalid pentest identifier '${sanitizeDisplay(options.pentest)}'. Use a display ID (e.g. PT-3) or UUID.`
46759
+ )
46760
+ );
46761
+ process.exit(1);
46762
+ }
46763
+ if (!isFindingIdentifier(options.finding)) {
46764
+ console.error(
46765
+ colorError(
46766
+ `Invalid finding identifier '${sanitizeDisplay(options.finding)}'. Use a finding number (e.g. PTF-7) or UUID.`
46767
+ )
46768
+ );
46769
+ process.exit(1);
46770
+ }
46771
+ const organizationId = await resolveOrganizationId();
46772
+ const result = await authenticatedRequest(
46773
+ (client) => client.pentestFinding.getCustomerFinding({
46774
+ params: {
46775
+ organizationId,
46776
+ pentestId: options.pentest,
46777
+ pentestFindingId: options.finding
46778
+ }
46779
+ })
46780
+ );
46781
+ if (result.status !== 200) {
46782
+ failGetFinding(result.status, result.body);
46783
+ }
46784
+ if (options.output === "json") {
46785
+ console.log(JSON.stringify(result.body, null, 2));
46786
+ return;
46787
+ }
46788
+ process.stdout.write(formatFindingMarkdown(result.body));
46789
+ }
46790
+ function createGetFindingCommand() {
46791
+ return new Command("get").description("Fetch one pentest finding with its full write-up").requiredOption("--pentest <id>", "Pentest display ID (e.g. PT-3) or UUID").requiredOption("--finding <id>", "Finding number (e.g. PTF-7) or UUID").option("-o, --output <format>", "Output format: text (markdown), json (default: text)", "text").addHelpText(
46792
+ "after",
46793
+ `
46794
+ One finding, every section: description, impact, attack scenario, proof of
46795
+ concept, root cause, remediation, severity considerations, and verification,
46796
+ plus the affected repositories and lines. The default output is markdown, ready
46797
+ to paste into a coding agent; --output json returns the API response.
46798
+
46799
+ Examples:
46800
+ tracecli pentest findings get --pentest PT-3 --finding PTF-7
46801
+ tracecli pentest findings get --pentest PT-3 --finding 7 --output json
46802
+ tracecli pentest findings get --pentest PT-3 --finding PTF-7 > finding.md
46803
+ `
46804
+ ).action(async (options) => {
46805
+ await runGetFinding(options);
46806
+ });
46807
+ }
46808
+
46809
+ // src/pentest/questionnaire.ts
46810
+ function buildUpsertQuestionnaireBody(answers) {
46811
+ if (answers === null) {
46812
+ return { answers: null };
46813
+ }
46814
+ return { answers: normalizePentestCustomerIntakeAnswers(answers) };
46815
+ }
46816
+ function parseQuestionnaireAnswersFile(raw, sourceLabel = "file") {
46817
+ let parsed;
46818
+ try {
46819
+ parsed = JSON.parse(raw);
46820
+ } catch {
46821
+ return { error: `Invalid JSON in ${sourceLabel}.` };
46822
+ }
46823
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
46824
+ return {
46825
+ error: `${sourceLabel} must be a JSON object of questionnaire answers (not an array or null).`
46826
+ };
46827
+ }
46828
+ if ("answers" in parsed && Object.keys(parsed).length === 1) {
46829
+ return {
46830
+ error: `${sourceLabel} must contain the answers object directly, not an { answers } wrapper. The CLI wraps it for the API.`
46831
+ };
46832
+ }
46833
+ const result = PentestCustomerIntakeAnswersSchema.safeParse(parsed);
46834
+ if (!result.success) {
46835
+ const issue2 = result.error.issues[0];
46836
+ const path = issue2?.path.length ? issue2.path.join(".") : "answers";
46837
+ const message = issue2?.message ?? "invalid";
46838
+ return { error: `Invalid questionnaire answers in ${sourceLabel}: ${path} \u2014 ${message}` };
46839
+ }
46840
+ return { answers: normalizePentestCustomerIntakeAnswers(result.data) };
46841
+ }
46842
+ function applyTextIntakeAnswer(current, key, input) {
46843
+ const trimmed = input.trim();
46844
+ if (trimmed.length === 0) {
46845
+ return current;
46846
+ }
46847
+ return { ...current, [key]: trimmed };
46848
+ }
46849
+ function applyMultiSelectIntakeAnswer(current, key, selected) {
46850
+ if (selected.length === 0) {
46851
+ return current;
46852
+ }
46853
+ return {
46854
+ ...current,
46855
+ [key]: Array.from(new Set(selected)).sort()
46856
+ };
46857
+ }
46858
+ function formatIssueClasses(values) {
46859
+ return values.map((v) => INTAKE_ISSUE_CLASS_LABELS[v]).join(", ");
46860
+ }
46861
+ function formatSurfaces(values) {
46862
+ return values.map((v) => INTAKE_SURFACE_LABELS[v]).join(", ");
46863
+ }
46864
+ function formatAnswerValue(key, answers) {
46865
+ switch (key) {
46866
+ case "issueClasses": {
46867
+ const values = answers.issueClasses;
46868
+ if (values === void 0 || values.length === 0) {
46869
+ return null;
46870
+ }
46871
+ return formatIssueClasses(values);
46872
+ }
46873
+ case "surfaces": {
46874
+ const values = answers.surfaces;
46875
+ if (values === void 0 || values.length === 0) {
46876
+ return null;
46877
+ }
46878
+ return formatSurfaces(values);
46879
+ }
46880
+ default: {
46881
+ const value = answers[key];
46882
+ if (value === void 0 || value.trim().length === 0) {
46883
+ return null;
46884
+ }
46885
+ return value;
46886
+ }
46887
+ }
46888
+ }
46889
+ function formatQuestionnaireShow(input) {
46890
+ const submitted = input.submitted === true;
46891
+ const editableLine = input.editable ? "yes (scoping window open)" : submitted ? "no (submitted)" : "no (editing closed after scoping)";
46892
+ const lines = [`Editable: ${editableLine}`, `Submitted: ${submitted ? "yes" : "no"}`];
46893
+ if (input.answers === null || Object.keys(input.answers).length === 0) {
46894
+ lines.push("Answers: (none)");
46895
+ return lines.join("\n");
46896
+ }
46897
+ lines.push("Answers:");
46898
+ for (const key of PENTEST_CUSTOMER_INTAKE_ANSWER_KEYS) {
46899
+ const value = formatAnswerValue(key, input.answers);
46900
+ if (value === null) {
46901
+ continue;
46902
+ }
46903
+ lines.push(`${PENTEST_CUSTOMER_INTAKE_LABELS[key]}: ${value}`);
46904
+ }
46905
+ return lines.join("\n");
46906
+ }
46907
+ function issueClassSelectOptions() {
46908
+ return CustomerPriorityCategoryValues.map((value) => ({
46909
+ value,
46910
+ label: INTAKE_ISSUE_CLASS_LABELS[value]
46911
+ }));
46912
+ }
46913
+ function surfaceSelectOptions() {
46914
+ return IntakeSurfaceValues.map((value) => ({
46915
+ value,
46916
+ label: INTAKE_SURFACE_LABELS[value]
46917
+ }));
46918
+ }
46919
+
46780
46920
  // src/pentest/commands.ts
46781
46921
  var DEFAULT_LIMIT2 = 30;
46782
46922
  var MAX_LIMIT2 = 100;
@@ -47147,6 +47287,7 @@ function renderPentestFindingLines(finding) {
47147
47287
  function createFindingsCommand() {
47148
47288
  const findings = new Command("findings").description("Manage pentest findings");
47149
47289
  findings.addCommand(createFindingsListCommand());
47290
+ findings.addCommand(createGetFindingCommand());
47150
47291
  findings.addCommand(createStatusUpdateCommand());
47151
47292
  findings.addCommand(createAttachEvidenceCommand());
47152
47293
  findings.addCommand(createCreateFindingCommand(), { hidden: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securewithtrace",
3
- "version": "0.42.0",
3
+ "version": "0.43.0",
4
4
  "description": "Official Trace CLI for vulnerability intelligence in your terminal",
5
5
  "type": "module",
6
6
  "bin": {