securewithtrace 0.21.7 → 0.23.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 +27 -5
  2. package/dist/index.js +400 -87
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -66,7 +66,8 @@ tracecli vulnerabilities list --severity HIGH
66
66
  ```bash
67
67
  tracecli auth login
68
68
  tracecli auth status
69
- tracecli auth logout
69
+ tracecli auth logout # prompts for confirmation interactively
70
+ tracecli auth logout --no-confirm # skip prompt (for CI/scripts)
70
71
  ```
71
72
 
72
73
  ### Organization context
@@ -86,14 +87,23 @@ When your account belongs to multiple organizations, use `tracecli org switch` t
86
87
  # List org repositories known to Trace
87
88
  tracecli repo list
88
89
 
90
+ # Show the next page of repositories
91
+ tracecli repo list --page 2 --limit 30
92
+
93
+ # Show all matching repositories
94
+ tracecli repo list --all
95
+
89
96
  # List only connected GitHub repositories that match "trace"
90
97
  tracecli repo list --platform GITHUB --status CONNECTED --query trace
91
98
 
92
99
  # Advanced filtering syntax (repeatable)
93
100
  tracecli repo list --filter "platform=GITHUB" --filter "repo:trace" --filter "status!=DISCONNECTED"
94
101
 
95
- # Alias command for filtering
102
+ # Alias command for filtering (all repo list flags are supported)
96
103
  tracecli repo filter --filter "language:typescript" --filter "branch=main"
104
+ tracecli repo filter --language typescript --limit 10
105
+ tracecli repo filter --output json | jq '.[].repositoryUrl'
106
+ tracecli repo filter --output tsv | cut -f1
97
107
 
98
108
  # Find repositories that use a specific dependency
99
109
  tracecli repo list --dependency lodash
@@ -105,7 +115,7 @@ tracecli repo list --dependency lodash@4.17.21
105
115
  tracecli repo list --dependency @types/node@20
106
116
 
107
117
  # Dependency results as JSON
108
- tracecli repo list --dependency react --json | jq '.[].fullName'
118
+ tracecli repo list --dependency react --output json | jq '.[].fullName'
109
119
 
110
120
  # Clone by owner/repo shorthand
111
121
  tracecli repo clone securewithtrace/trace
@@ -117,6 +127,16 @@ tracecli repo clone securewithtrace/trace --branch main --depth 1
117
127
  ### Vulnerabilities
118
128
 
119
129
  ```bash
130
+ # Fetch a single vulnerability by display ID, numeric ID, or UUID
131
+ tracecli vulnerabilities get TRACE-123
132
+
133
+ # Open the vulnerability detail page in a browser
134
+ tracecli vulnerabilities get TRACE-123 --web
135
+
136
+ # JSON or TSV output for scripting
137
+ tracecli vulnerabilities get TRACE-123 --output json
138
+ tracecli vulnerabilities get TRACE-123 --output tsv | cut -f1,10
139
+
120
140
  # List vulnerabilities across your organization (default: 30 results)
121
141
  tracecli vulnerabilities list
122
142
 
@@ -173,6 +193,8 @@ Repository filter options:
173
193
  - Supported fields: `platform`, `status`, `url`, `repo`, `branch`, `language`
174
194
  - Supported operators: `=` (exact), `!=` (not exact), `:` (contains), `!:` (not contains)
175
195
  - `--dependency`: filter by dependency name with optional `@version` (e.g. `lodash`, `lodash@4.17`, `@types/node@20`)
196
+ - `--page`: choose which page of results to display when using `--limit` (default: 1)
197
+ - `--all`: display all matching repositories (disables pagination)
176
198
 
177
199
  ### Pentests
178
200
 
@@ -198,7 +220,7 @@ tracecli pentest findings list --pentest PT-3 --output json
198
220
  # Findings as TSV
199
221
  tracecli pentest findings list --pentest PT-3 --output tsv | cut -f1,4
200
222
 
201
- # Open pentests dashboard in browser
223
+ # Open the pentest detail page in a browser
202
224
  tracecli pentest findings list --pentest PT-3 --web
203
225
  ```
204
226
 
@@ -216,7 +238,7 @@ Flags for `pentest findings list`:
216
238
  |------|-----------|-------------|
217
239
  | `--pentest <id>` | | Pentest display ID (e.g. `PT-3`) or UUID (required) |
218
240
  | `--output <format>` | `-o` | Output format: `table` (default), `json`, `tsv` |
219
- | `--web` | `-w` | Open pentests dashboard in browser |
241
+ | `--web` | `-w` | Open the pentest detail page in a browser |
220
242
 
221
243
  ### Upgrades
222
244
 
package/dist/index.js CHANGED
@@ -24600,6 +24600,15 @@ var ScanMode2 = external_exports.enum(ScanModeValues2);
24600
24600
  var VulnerabilityIdParamsSchema = OrgIdParamsSchema.extend({
24601
24601
  vulnerabilityId: external_exports.uuid()
24602
24602
  });
24603
+ var VulnerabilityLookupIdSchema = external_exports.union([
24604
+ external_exports.uuid(),
24605
+ external_exports.string().regex(/^(?:TRACE-)?0*[1-9]\d*$/i, {
24606
+ message: "vulnerabilityId must be a UUID, a positive integer, or TRACE-<number>"
24607
+ })
24608
+ ]);
24609
+ var VulnerabilityLookupIdParamsSchema = OrgIdParamsSchema.extend({
24610
+ vulnerabilityId: VulnerabilityLookupIdSchema
24611
+ });
24603
24612
  var VulnerabilitySourceSchema = external_exports.object({
24604
24613
  branch: external_exports.string().nullable(),
24605
24614
  commitSha: external_exports.string().nullable(),
@@ -24802,9 +24811,10 @@ var vulnerabilityContract = c17.router({
24802
24811
  getVulnerability: {
24803
24812
  method: "GET",
24804
24813
  path: "/organizations/:organizationId/vulnerabilities/:vulnerabilityId",
24805
- pathParams: VulnerabilityIdParamsSchema,
24814
+ pathParams: VulnerabilityLookupIdParamsSchema,
24806
24815
  responses: {
24807
24816
  200: VulnerabilityResponseSchema,
24817
+ 400: ErrorResponseSchema,
24808
24818
  401: ErrorResponseSchema,
24809
24819
  403: ErrorResponseSchema,
24810
24820
  404: ErrorResponseSchema,
@@ -24814,7 +24824,7 @@ var vulnerabilityContract = c17.router({
24814
24824
  updateVulnerability: {
24815
24825
  method: "PATCH",
24816
24826
  path: "/organizations/:organizationId/vulnerabilities/:vulnerabilityId",
24817
- pathParams: VulnerabilityIdParamsSchema,
24827
+ pathParams: VulnerabilityLookupIdParamsSchema,
24818
24828
  body: UpdateVulnerabilityRequestSchema,
24819
24829
  responses: {
24820
24830
  200: VulnerabilityResponseSchema,
@@ -24842,7 +24852,7 @@ var vulnerabilityContract = c17.router({
24842
24852
  deleteVulnerability: {
24843
24853
  method: "DELETE",
24844
24854
  path: "/organizations/:organizationId/vulnerabilities/:vulnerabilityId",
24845
- pathParams: VulnerabilityIdParamsSchema,
24855
+ pathParams: VulnerabilityLookupIdParamsSchema,
24846
24856
  responses: {
24847
24857
  204: DeleteResponseSchema,
24848
24858
  401: ErrorResponseSchema,
@@ -24854,7 +24864,7 @@ var vulnerabilityContract = c17.router({
24854
24864
  retestVulnerability: {
24855
24865
  method: "POST",
24856
24866
  path: "/organizations/:organizationId/vulnerabilities/:vulnerabilityId/retest",
24857
- pathParams: VulnerabilityIdParamsSchema,
24867
+ pathParams: VulnerabilityLookupIdParamsSchema,
24858
24868
  body: null,
24859
24869
  responses: {
24860
24870
  201: ScanResponseSchema,
@@ -24881,7 +24891,7 @@ var vulnerabilityContract = c17.router({
24881
24891
  getVulnerabilityRetests: {
24882
24892
  method: "GET",
24883
24893
  path: "/organizations/:organizationId/vulnerabilities/:vulnerabilityId/retests",
24884
- pathParams: VulnerabilityIdParamsSchema,
24894
+ pathParams: VulnerabilityLookupIdParamsSchema,
24885
24895
  responses: {
24886
24896
  200: ScanArrayResponseSchema,
24887
24897
  401: ErrorResponseSchema,
@@ -24893,10 +24903,10 @@ var vulnerabilityContract = c17.router({
24893
24903
  });
24894
24904
 
24895
24905
  // ../../packages/api-contracts/src/schemas/api/v1/vulnerability-note.api.ts
24896
- var VulnerabilityNoteParamsSchema = OrgIdParamsSchema.extend({
24897
- vulnerabilityId: external_exports.uuid()
24906
+ var VulnerabilityLookupNoteParamsSchema = OrgIdParamsSchema.extend({
24907
+ vulnerabilityId: VulnerabilityLookupIdSchema
24898
24908
  });
24899
- var VulnerabilityNoteIdParamsSchema = VulnerabilityNoteParamsSchema.extend({
24909
+ var VulnerabilityLookupNoteIdParamsSchema = VulnerabilityLookupNoteParamsSchema.extend({
24900
24910
  noteId: external_exports.uuid()
24901
24911
  });
24902
24912
  var VulnerabilityNoteResponseSchema = external_exports.object({
@@ -24924,9 +24934,10 @@ var vulnerabilityNoteContract = c18.router({
24924
24934
  listNotes: {
24925
24935
  method: "GET",
24926
24936
  path: "/organizations/:organizationId/vulnerabilities/:vulnerabilityId/notes",
24927
- pathParams: VulnerabilityNoteParamsSchema,
24937
+ pathParams: VulnerabilityLookupNoteParamsSchema,
24928
24938
  responses: {
24929
24939
  200: ListVulnerabilityNotesResponseSchema,
24940
+ 400: ErrorResponseSchema,
24930
24941
  401: ErrorResponseSchema,
24931
24942
  403: ErrorResponseSchema,
24932
24943
  404: ErrorResponseSchema,
@@ -24936,7 +24947,7 @@ var vulnerabilityNoteContract = c18.router({
24936
24947
  createNote: {
24937
24948
  method: "POST",
24938
24949
  path: "/organizations/:organizationId/vulnerabilities/:vulnerabilityId/notes",
24939
- pathParams: VulnerabilityNoteParamsSchema,
24950
+ pathParams: VulnerabilityLookupNoteParamsSchema,
24940
24951
  body: CreateVulnerabilityNoteRequestSchema,
24941
24952
  responses: {
24942
24953
  201: VulnerabilityNoteResponseSchema,
@@ -24950,7 +24961,7 @@ var vulnerabilityNoteContract = c18.router({
24950
24961
  updateNote: {
24951
24962
  method: "PATCH",
24952
24963
  path: "/organizations/:organizationId/vulnerabilities/:vulnerabilityId/notes/:noteId",
24953
- pathParams: VulnerabilityNoteIdParamsSchema,
24964
+ pathParams: VulnerabilityLookupNoteIdParamsSchema,
24954
24965
  body: UpdateVulnerabilityNoteRequestSchema,
24955
24966
  responses: {
24956
24967
  200: VulnerabilityNoteResponseSchema,
@@ -24964,9 +24975,10 @@ var vulnerabilityNoteContract = c18.router({
24964
24975
  deleteNote: {
24965
24976
  method: "DELETE",
24966
24977
  path: "/organizations/:organizationId/vulnerabilities/:vulnerabilityId/notes/:noteId",
24967
- pathParams: VulnerabilityNoteIdParamsSchema,
24978
+ pathParams: VulnerabilityLookupNoteIdParamsSchema,
24968
24979
  responses: {
24969
24980
  204: DeleteResponseSchema,
24981
+ 400: ErrorResponseSchema,
24970
24982
  401: ErrorResponseSchema,
24971
24983
  403: ErrorResponseSchema,
24972
24984
  404: ErrorResponseSchema,
@@ -25051,7 +25063,7 @@ var WebhookEventTypeSchema = external_exports.enum([
25051
25063
  ]);
25052
25064
  var WebhookEndpointStatusSchema = external_exports.enum(["enabled", "disabled"]);
25053
25065
  var WebhookDeliveryStatusSchema = external_exports.enum(["succeeded", "failed"]);
25054
- var WebhookDeliverySourceSchema = external_exports.enum(["test", "mock"]);
25066
+ var WebhookDeliverySourceSchema = external_exports.enum(["test", "mock", "event"]);
25055
25067
  var WebhookEndpointSchema = external_exports.object({
25056
25068
  webhookId: external_exports.uuid(),
25057
25069
  organizationId: external_exports.uuid(),
@@ -26535,6 +26547,44 @@ function getClientId() {
26535
26547
  );
26536
26548
  }
26537
26549
  var SAFE_ORGANIZATION_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
26550
+ var ORG_FILTER_FIELDS = ["name", "slug"];
26551
+ function parseOrgFilter(expression) {
26552
+ const match = expression.match(/^([^=!:]+)(!=|!:|=|:)(.*)$/);
26553
+ if (!match || !match[1] || !match[2] || match[3] === void 0) {
26554
+ throw new Error(
26555
+ `Invalid filter expression: '${expression}'. Format: <field><operator><value> (e.g. name:acme)`
26556
+ );
26557
+ }
26558
+ const field = match[1].trim();
26559
+ const operator = match[2];
26560
+ const value = match[3].trim();
26561
+ if (!ORG_FILTER_FIELDS.includes(field)) {
26562
+ throw new Error(
26563
+ `Unknown filter field '${field}'. Supported fields: ${ORG_FILTER_FIELDS.join(", ")}`
26564
+ );
26565
+ }
26566
+ if (value.length === 0) {
26567
+ throw new Error(
26568
+ `Invalid filter expression: '${expression}'. Value cannot be empty after the operator.`
26569
+ );
26570
+ }
26571
+ return { field, operator, value };
26572
+ }
26573
+ function matchesOrgFilter(item, filter) {
26574
+ const raw = filter.field === "name" ? item.name : item.slug;
26575
+ const cellLower = raw.toLowerCase();
26576
+ const valueLower = filter.value.toLowerCase();
26577
+ switch (filter.operator) {
26578
+ case "=":
26579
+ return cellLower === valueLower;
26580
+ case "!=":
26581
+ return cellLower !== valueLower;
26582
+ case ":":
26583
+ return cellLower.includes(valueLower);
26584
+ case "!:":
26585
+ return !cellLower.includes(valueLower);
26586
+ }
26587
+ }
26538
26588
  function sanitizeForOutput(value) {
26539
26589
  return value.replace(/[\r\n\t]/g, " ").trim();
26540
26590
  }
@@ -26784,12 +26834,25 @@ ${colorSuccess(`Authenticated as ${displayName} (${tokenResponse.user.email})`)}
26784
26834
  });
26785
26835
  }
26786
26836
  function createLogoutCommand() {
26787
- return new Command("logout").description("Log out of Trace").addHelpText(
26837
+ return new Command("logout").description("Log out of Trace").option("--no-confirm", "Skip confirmation prompt (for CI/scripts)").addHelpText(
26788
26838
  "after",
26789
26839
  `
26790
26840
  Examples:
26791
- tracecli auth logout`
26792
- ).action(() => {
26841
+ tracecli auth logout
26842
+ tracecli auth logout --no-confirm`
26843
+ ).action(async (options) => {
26844
+ if (options.confirm && process.stdin.isTTY && process.stdout.isTTY) {
26845
+ const readline = createInterface2({ input: process.stdin, output: process.stdout });
26846
+ try {
26847
+ const answer = await readline.question("Are you sure you want to log out? [Y/n] ");
26848
+ if (answer.trim().toLowerCase() === "n" || answer.trim().toLowerCase() === "no") {
26849
+ console.log("Logout cancelled.");
26850
+ return;
26851
+ }
26852
+ } finally {
26853
+ readline.close();
26854
+ }
26855
+ }
26793
26856
  const removed = removeToken();
26794
26857
  if (removed) {
26795
26858
  console.log(
@@ -26845,6 +26908,9 @@ Examples:
26845
26908
  const info = buildAuthStatusInfo(token, refreshToken, selectedOrganizationId);
26846
26909
  if (options.output === "json") {
26847
26910
  console.log(JSON.stringify(info, null, 2));
26911
+ if (!info.loggedIn) {
26912
+ process.exit(1);
26913
+ }
26848
26914
  return;
26849
26915
  }
26850
26916
  if (options.output === "tsv") {
@@ -26859,11 +26925,14 @@ Examples:
26859
26925
  info.activeOrg ?? ""
26860
26926
  ].join(" ")
26861
26927
  );
26928
+ if (!info.loggedIn) {
26929
+ process.exit(1);
26930
+ }
26862
26931
  return;
26863
26932
  }
26864
26933
  if (!info.loggedIn) {
26865
- console.log(colorError("Not logged in. Run `tracecli auth login` to authenticate."));
26866
- return;
26934
+ console.error(colorError("Not logged in. Run `tracecli auth login` to authenticate."));
26935
+ process.exit(1);
26867
26936
  }
26868
26937
  console.log("Logged in to Trace");
26869
26938
  if (info.userId) {
@@ -26903,7 +26972,12 @@ function createOrgListCommand() {
26903
26972
  throw new Error("--limit must be a positive integer");
26904
26973
  }
26905
26974
  return n;
26906
- }).option("-w, --web", "Open the organization settings in a browser").addHelpText(
26975
+ }).option(
26976
+ "-f, --filter <expression>",
26977
+ "Filter expression (repeatable). Fields: name, slug. Operators: = exact, != not exact, : contains, !: not contains",
26978
+ (val, prev) => [...prev, val],
26979
+ []
26980
+ ).option("-w, --web", "Open the organization settings in a browser").addHelpText(
26907
26981
  "after",
26908
26982
  `
26909
26983
  Examples:
@@ -26911,70 +26985,88 @@ Examples:
26911
26985
  tracecli org list --output json
26912
26986
  tracecli org list --output tsv | cut -f1
26913
26987
  tracecli org list --limit 5
26988
+ tracecli org list --filter "name:acme"
26989
+ tracecli org list --filter "slug!=internal"
26914
26990
  tracecli org list --web`
26915
- ).action(async (options) => {
26916
- if (options.web) {
26917
- const slug = await resolveOrganizationSlug();
26918
- const url2 = `${getAppBaseUrl()}/dashboard/${slug}/settings`;
26919
- const opened = await openBrowser(url2);
26920
- if (!opened) {
26921
- console.log(`Open in browser: ${url2}`);
26991
+ ).action(
26992
+ async (options) => {
26993
+ if (options.web) {
26994
+ const slug = await resolveOrganizationSlug();
26995
+ const url2 = `${getAppBaseUrl()}/dashboard/${slug}/settings`;
26996
+ const opened = await openBrowser(url2);
26997
+ if (!opened) {
26998
+ console.log(`Open in browser: ${url2}`);
26999
+ }
27000
+ return;
26922
27001
  }
26923
- return;
26924
- }
26925
- const organizationContext = await fetchOrganizationContext();
26926
- const allItems = organizationContext.organizations.map(
26927
- (organization) => ({
26928
- ...organization,
26929
- active: organization.organizationId === organizationContext.activeOrganization.organizationId,
26930
- selected: organization.organizationId === organizationContext.selectedOrganizationId
26931
- })
26932
- );
26933
- const items = options.limit !== void 0 ? allItems.slice(0, options.limit) : allItems;
26934
- if (options.output === "json") {
26935
- console.log(JSON.stringify(items, null, 2));
26936
- return;
26937
- }
26938
- if (options.output === "tsv") {
26939
- console.log("name slug organizationId active");
26940
- for (const item of items) {
27002
+ const parsedFilters = [];
27003
+ for (const expr of options.filter) {
27004
+ try {
27005
+ parsedFilters.push(parseOrgFilter(expr));
27006
+ } catch (err) {
27007
+ console.error(err instanceof Error ? err.message : String(err));
27008
+ process.exit(1);
27009
+ }
27010
+ }
27011
+ const organizationContext = await fetchOrganizationContext();
27012
+ const allItems = organizationContext.organizations.map(
27013
+ (organization) => ({
27014
+ ...organization,
27015
+ active: organization.organizationId === organizationContext.activeOrganization.organizationId,
27016
+ selected: organization.organizationId === organizationContext.selectedOrganizationId
27017
+ })
27018
+ );
27019
+ const filteredItems = allItems.filter(
27020
+ (item) => parsedFilters.every((f) => matchesOrgFilter(item, f))
27021
+ );
27022
+ const items = options.limit !== void 0 ? filteredItems.slice(0, options.limit) : filteredItems;
27023
+ if (options.output === "json") {
27024
+ console.log(JSON.stringify(items, null, 2));
27025
+ return;
27026
+ }
27027
+ if (options.output === "tsv") {
27028
+ console.log("name slug organizationId active");
27029
+ for (const item of items) {
27030
+ console.log(
27031
+ [
27032
+ tsvSafe(item.name),
27033
+ tsvSafe(item.slug),
27034
+ tsvSafe(item.organizationId),
27035
+ item.active ? "true" : "false"
27036
+ ].join(" ")
27037
+ );
27038
+ }
27039
+ return;
27040
+ }
27041
+ const headers = ["", "Name", "Slug", "Org ID"];
27042
+ const rows = items.map((item) => {
27043
+ const labels = [item.active ? "active" : null, item.selected ? "selected" : null].filter(
27044
+ Boolean
27045
+ );
27046
+ const marker = item.active ? "*" : " ";
27047
+ const nameSuffix = labels.length > 0 ? ` [${labels.join(", ")}]` : "";
27048
+ return [
27049
+ marker,
27050
+ sanitizeForOutput(item.name) + nameSuffix,
27051
+ sanitizeForOutput(item.slug),
27052
+ item.organizationId
27053
+ ];
27054
+ });
27055
+ const colWidths = headers.map(
27056
+ (h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length))
27057
+ );
27058
+ const pad = (s, w3) => s.padEnd(w3);
27059
+ console.log(headers.map((h, i) => pad(h, colWidths[i] ?? h.length)).join(" "));
27060
+ for (const row of rows) {
27061
+ console.log(row.map((cell, i) => pad(cell, colWidths[i] ?? 0)).join(" "));
27062
+ }
27063
+ if (organizationContext.staleSelectionDetected) {
26941
27064
  console.log(
26942
- [
26943
- tsvSafe(item.name),
26944
- tsvSafe(item.slug),
26945
- tsvSafe(item.organizationId),
26946
- item.active ? "true" : "false"
26947
- ].join(" ")
27065
+ "\nYour previous organization selection is no longer available and was reset."
26948
27066
  );
26949
27067
  }
26950
- return;
26951
- }
26952
- const headers = ["", "Name", "Slug", "Org ID"];
26953
- const rows = items.map((item) => {
26954
- const labels = [item.active ? "active" : null, item.selected ? "selected" : null].filter(
26955
- Boolean
26956
- );
26957
- const marker = item.active ? "*" : " ";
26958
- const nameSuffix = labels.length > 0 ? ` [${labels.join(", ")}]` : "";
26959
- return [
26960
- marker,
26961
- sanitizeForOutput(item.name) + nameSuffix,
26962
- sanitizeForOutput(item.slug),
26963
- item.organizationId
26964
- ];
26965
- });
26966
- const colWidths = headers.map(
26967
- (h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length))
26968
- );
26969
- const pad = (s, w3) => s.padEnd(w3);
26970
- console.log(headers.map((h, i) => pad(h, colWidths[i] ?? h.length)).join(" "));
26971
- for (const row of rows) {
26972
- console.log(row.map((cell, i) => pad(cell, colWidths[i] ?? 0)).join(" "));
26973
- }
26974
- if (organizationContext.staleSelectionDetected) {
26975
- console.log("\nYour previous organization selection is no longer available and was reset.");
26976
27068
  }
26977
- });
27069
+ );
26978
27070
  }
26979
27071
  function createOrgCurrentCommand() {
26980
27072
  return new Command("current").description("Show the active organization used by CLI commands").option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").option("-w, --web", "Open the organization settings in a browser").addHelpText(
@@ -27031,15 +27123,42 @@ Examples:
27031
27123
  });
27032
27124
  }
27033
27125
  function createOrgSwitchCommand() {
27034
- return new Command("switch").description("Switch the active organization for subsequent CLI commands").argument("[organization]", "Organization ID, slug, name, or menu selection index").addHelpText(
27126
+ return new Command("switch").description("Switch the active organization for subsequent CLI commands").argument("[organization]", "Organization ID, slug, name, or menu selection index").option("-w, --web", "Open the organization settings page in a browser instead of switching").addHelpText(
27035
27127
  "after",
27036
27128
  `
27037
27129
  Examples:
27038
27130
  tracecli org switch
27039
27131
  tracecli org switch engineering
27040
- tracecli org switch org_01KJTJFFG497Z1C54Z07AP6NA3`
27041
- ).action(async (organization) => {
27132
+ tracecli org switch org_01KJTJFFG497Z1C54Z07AP6NA3
27133
+ tracecli org switch engineering --web`
27134
+ ).action(async (organization, options) => {
27042
27135
  const normalizedOrganizationInput = organization?.trim() ?? "";
27136
+ if (options.web) {
27137
+ if (normalizedOrganizationInput.length > 0) {
27138
+ const organizationContext2 = await fetchOrganizationContext();
27139
+ const selection = resolveOrganizationSelectionInput(
27140
+ organizationContext2.organizations,
27141
+ normalizedOrganizationInput
27142
+ );
27143
+ if ("error" in selection) {
27144
+ console.error(selection.error);
27145
+ process.exit(1);
27146
+ }
27147
+ const url2 = `${getAppBaseUrl()}/dashboard/${selection.organization.slug}/settings`;
27148
+ const opened = await openBrowser(url2);
27149
+ if (!opened) {
27150
+ console.log(`Open in browser: ${url2}`);
27151
+ }
27152
+ } else {
27153
+ const slug = await resolveOrganizationSlug();
27154
+ const url2 = `${getAppBaseUrl()}/dashboard/${slug}/settings`;
27155
+ const opened = await openBrowser(url2);
27156
+ if (!opened) {
27157
+ console.log(`Open in browser: ${url2}`);
27158
+ }
27159
+ }
27160
+ return;
27161
+ }
27043
27162
  if (normalizedOrganizationInput.length === 0 && (!process.stdin.isTTY || !process.stdout.isTTY)) {
27044
27163
  console.error(
27045
27164
  "Missing required argument 'organization'. Run `tracecli org switch <organization>` in non-interactive environments."
@@ -27705,7 +27824,7 @@ function createFindingsListCommand() {
27705
27824
  }
27706
27825
  return Math.min(n, MAX_LIMIT);
27707
27826
  }
27708
- ).option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").option("-w, --web", "Open the pentests dashboard in a browser").addHelpText(
27827
+ ).option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").option("-w, --web", "Open the pentest detail page in a browser").addHelpText(
27709
27828
  "after",
27710
27829
  `
27711
27830
  Examples:
@@ -27719,7 +27838,9 @@ Examples:
27719
27838
  async (options) => {
27720
27839
  if (options.web) {
27721
27840
  const slug = await resolveOrganizationSlug();
27722
- const url2 = `${getAppBaseUrl()}/dashboard/${slug}/pentests`;
27841
+ const numericMatch = options.pentest.match(/^(?:PT-)?(\d+)$/i);
27842
+ const pentestUrlId = numericMatch?.[1] ?? options.pentest;
27843
+ const url2 = `${getAppBaseUrl()}/dashboard/${slug}/pentests/${pentestUrlId}`;
27723
27844
  const opened = await openBrowser(url2);
27724
27845
  if (!opened) {
27725
27846
  console.log(`Open in browser: ${url2}`);
@@ -27873,6 +27994,7 @@ function extractRepoName(input) {
27873
27994
  var BRANCH_NAME_PATTERN = /^[a-zA-Z0-9._/+-]+$/;
27874
27995
  var TRACE_GIT_ASKPASS_TOKEN_ENV = "TRACE_GIT_ASKPASS_TOKEN";
27875
27996
  var DEFAULT_LIMIT2 = 30;
27997
+ var DEFAULT_PAGE = 1;
27876
27998
  var MAX_LIMIT2 = 100;
27877
27999
  var REPOSITORY_PLATFORMS = ["GITHUB", "GITLAB", "BITBUCKET"];
27878
28000
  var REPOSITORY_STATUSES = ["CONNECTED", "DISCONNECTED"];
@@ -28090,6 +28212,44 @@ function parseListLimit(value) {
28090
28212
  }
28091
28213
  return n;
28092
28214
  }
28215
+ function parseListPage(value) {
28216
+ const n = Number(value);
28217
+ if (!Number.isInteger(n) || n < 1) {
28218
+ throw new Error("--page must be a positive integer");
28219
+ }
28220
+ return n;
28221
+ }
28222
+ function paginateResults(items, options) {
28223
+ if (options.all) {
28224
+ return {
28225
+ items: [...items],
28226
+ pagination: null
28227
+ };
28228
+ }
28229
+ const limit = options.limit ?? DEFAULT_LIMIT2;
28230
+ const page = options.page ?? DEFAULT_PAGE;
28231
+ const totalItems = items.length;
28232
+ const totalPages = Math.max(1, Math.ceil(totalItems / limit));
28233
+ if (page > totalPages) {
28234
+ throw new Error(
28235
+ `--page ${page} is out of range for ${totalItems} matching results with --limit ${limit}. Use --page between 1 and ${totalPages}, or pass --all.`
28236
+ );
28237
+ }
28238
+ const startIndex = (page - 1) * limit;
28239
+ const endIndex = Math.min(startIndex + limit, totalItems);
28240
+ return {
28241
+ items: items.slice(startIndex, endIndex),
28242
+ pagination: {
28243
+ totalItems,
28244
+ totalPages,
28245
+ page,
28246
+ hasNextPage: page < totalPages,
28247
+ hasPreviousPage: page > 1,
28248
+ startIndex,
28249
+ endIndex
28250
+ }
28251
+ };
28252
+ }
28093
28253
  function printRepositoriesTsv(repositories) {
28094
28254
  console.log("repositoryUrl platform status defaultBranch language");
28095
28255
  for (const repo of repositories) {
@@ -28126,6 +28286,19 @@ function collectFilterExpression(value, previous) {
28126
28286
  previous.push(value);
28127
28287
  return previous;
28128
28288
  }
28289
+ function printPaginationHint(pagination, noun, nextPageFilterHint) {
28290
+ if (!pagination || pagination.totalPages <= 1) {
28291
+ return;
28292
+ }
28293
+ const shownStart = pagination.startIndex + 1;
28294
+ const shownEnd = pagination.endIndex;
28295
+ console.log(
28296
+ `Showing ${shownStart}-${shownEnd} of ${pagination.totalItems} ${noun} (page ${pagination.page}/${pagination.totalPages}).`
28297
+ );
28298
+ if (pagination.hasNextPage) {
28299
+ console.log(`Use --page ${pagination.page + 1}${nextPageFilterHint} (or --all) to see more.`);
28300
+ }
28301
+ }
28129
28302
  function extractErrorMessage(body) {
28130
28303
  if (typeof body === "object" && body !== null && "message" in body && typeof body.message === "string") {
28131
28304
  const message = body.message.trim();
@@ -28251,6 +28424,11 @@ function addRepositoryFilterOptions(command) {
28251
28424
  parseListLimit,
28252
28425
  DEFAULT_LIMIT2
28253
28426
  ).option(
28427
+ "--page <n>",
28428
+ `Page number to display when results exceed --limit (default ${DEFAULT_PAGE})`,
28429
+ parseListPage,
28430
+ DEFAULT_PAGE
28431
+ ).option("--all", "Display all matching repositories (disables pagination)").option(
28254
28432
  "--dependency <pkg>",
28255
28433
  "Filter repositories that use a dependency (e.g. lodash, lodash@4.17.21, @types/node@20)"
28256
28434
  ).option("-w, --web", "Open the repositories dashboard in a browser");
@@ -28302,8 +28480,17 @@ async function runRepositoryListCommand(options) {
28302
28480
  console.log("No repositories found.");
28303
28481
  return;
28304
28482
  }
28305
- const limit = options.limit ?? DEFAULT_LIMIT2;
28306
- const displayedRepositories = filteredRepositories.slice(0, limit);
28483
+ let displayedRepositories;
28484
+ let pagination = null;
28485
+ try {
28486
+ const paginatedResult = paginateResults(filteredRepositories, options);
28487
+ displayedRepositories = paginatedResult.items;
28488
+ pagination = paginatedResult.pagination;
28489
+ } catch (error48) {
28490
+ const message = error48 instanceof Error ? error48.message : "Unknown pagination error.";
28491
+ console.error(`Invalid pagination options: ${message}`);
28492
+ process.exit(1);
28493
+ }
28307
28494
  if (options.output === "json") {
28308
28495
  console.log(JSON.stringify(displayedRepositories, null, 2));
28309
28496
  return;
@@ -28313,6 +28500,11 @@ async function runRepositoryListCommand(options) {
28313
28500
  return;
28314
28501
  }
28315
28502
  printRepositoriesTable(displayedRepositories);
28503
+ printPaginationHint(
28504
+ pagination,
28505
+ "repositories",
28506
+ options.filter.length > 0 ? " with the same --filter flags" : ""
28507
+ );
28316
28508
  }
28317
28509
  async function runDependencyFilter(organizationId, options) {
28318
28510
  const dependency = options.dependency;
@@ -28366,8 +28558,17 @@ async function runDependencyFilter(organizationId, options) {
28366
28558
  console.log(`No repositories found using dependency '${label}'.`);
28367
28559
  return;
28368
28560
  }
28369
- const limit = options.limit ?? DEFAULT_LIMIT2;
28370
- const displayedRepos = repos.slice(0, limit);
28561
+ let displayedRepos;
28562
+ let pagination = null;
28563
+ try {
28564
+ const paginatedResult = paginateResults(repos, options);
28565
+ displayedRepos = paginatedResult.items;
28566
+ pagination = paginatedResult.pagination;
28567
+ } catch (error48) {
28568
+ const message = error48 instanceof Error ? error48.message : "Unknown pagination error.";
28569
+ console.error(`Invalid pagination options: ${message}`);
28570
+ process.exit(1);
28571
+ }
28371
28572
  if (options.output === "json") {
28372
28573
  console.log(JSON.stringify(displayedRepos, null, 2));
28373
28574
  return;
@@ -28383,6 +28584,7 @@ async function runDependencyFilter(organizationId, options) {
28383
28584
  const versions = repo.versions.join(", ");
28384
28585
  console.log(`${repo.fullName} (${versions})`);
28385
28586
  }
28587
+ printPaginationHint(pagination, "repositories", "");
28386
28588
  }
28387
28589
  function createListCommand2() {
28388
28590
  return addRepositoryFilterOptions(
@@ -28393,6 +28595,8 @@ function createListCommand2() {
28393
28595
  Examples:
28394
28596
  tracecli repo list
28395
28597
  tracecli repo list --platform GITHUB --status CONNECTED
28598
+ tracecli repo list --page 2 --limit 30
28599
+ tracecli repo list --all
28396
28600
  tracecli repo list --output json | jq '.[].repositoryUrl'
28397
28601
  tracecli repo list --output tsv | cut -f1
28398
28602
  tracecli repo list --filter "language:typescript" --limit 10
@@ -28410,7 +28614,11 @@ function createFilterCommand() {
28410
28614
  `
28411
28615
  Examples:
28412
28616
  tracecli repo filter --filter "platform=GITHUB" --filter "status=CONNECTED"
28413
- tracecli repo filter --language typescript --output json
28617
+ tracecli repo filter --page 2 --limit 30
28618
+ tracecli repo filter --all
28619
+ tracecli repo filter --language typescript --limit 10
28620
+ tracecli repo filter --output json | jq '.[].repositoryUrl'
28621
+ tracecli repo filter --output tsv | cut -f1
28414
28622
  tracecli repo filter --web`
28415
28623
  ).action(async (options) => {
28416
28624
  await runRepositoryListCommand(options);
@@ -29119,6 +29327,110 @@ function printVulnerabilitiesTsv(vulnerabilities) {
29119
29327
  console.log(cols.join(" "));
29120
29328
  }
29121
29329
  }
29330
+ function printVulnerabilityDetail(vulnerability) {
29331
+ const severity = colorSeverity(vulnerability.severity, formatSeverity(vulnerability.severity));
29332
+ const status = formatStatus(vulnerability.status);
29333
+ const metadata = [
29334
+ `Type: ${vulnerability.vulnerabilityType}`,
29335
+ vulnerability.scanMode ? `Scan mode: ${vulnerability.scanMode}` : null,
29336
+ vulnerability.cweId ? vulnerability.cweId : null
29337
+ ].filter((value) => value !== null);
29338
+ const sourceDetails = [
29339
+ vulnerability.source.branch ? `Branch: ${vulnerability.source.branch}` : null,
29340
+ vulnerability.source.pullRequestNumber !== null ? `PR #${vulnerability.source.pullRequestNumber}${vulnerability.source.pullRequestTitle ? ` (${vulnerability.source.pullRequestTitle})` : ""}` : null
29341
+ ].filter((value) => value !== null);
29342
+ console.log(` [${severity}] ${vulnerability.traceId} ${vulnerability.title}`);
29343
+ console.log(
29344
+ ` ${vulnerability.repositoryName} \xB7 ${vulnerability.codeSnippet.filePath}:${vulnerability.codeSnippet.startLine} \xB7 ${status}`
29345
+ );
29346
+ if (metadata.length > 0) {
29347
+ console.log(` ${metadata.join(" | ")}`);
29348
+ }
29349
+ if (sourceDetails.length > 0) {
29350
+ console.log(` ${sourceDetails.join(" | ")}`);
29351
+ }
29352
+ console.log(` Vulnerability ID: ${vulnerability.vulnerabilityId}`);
29353
+ console.log("");
29354
+ console.log(vulnerability.description);
29355
+ }
29356
+ function printVulnerabilityDetailTsv(vulnerability) {
29357
+ console.log(
29358
+ "traceId vulnerabilityId severity status repository file line cweId scanMode title description"
29359
+ );
29360
+ const cols = [
29361
+ vulnerability.traceId,
29362
+ vulnerability.vulnerabilityId,
29363
+ vulnerability.severity,
29364
+ vulnerability.status,
29365
+ vulnerability.repositoryName,
29366
+ vulnerability.codeSnippet.filePath,
29367
+ String(vulnerability.codeSnippet.startLine),
29368
+ vulnerability.cweId ?? "",
29369
+ vulnerability.scanMode ?? "",
29370
+ vulnerability.title,
29371
+ vulnerability.description
29372
+ ].map(sanitizeTsvField);
29373
+ console.log(cols.join(" "));
29374
+ }
29375
+ function createGetCommand() {
29376
+ return new Command("get").description("Get a vulnerability by display ID or UUID").argument("<id>", "Vulnerability display ID (e.g. TRACE-123 or 123) or UUID").option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").option("-w, --web", "Open the vulnerability detail page in a browser").addHelpText(
29377
+ "after",
29378
+ `
29379
+ Examples:
29380
+ tracecli vulnerabilities get TRACE-123
29381
+ tracecli vulnerabilities get 123
29382
+ tracecli vulnerabilities get 00000000-0000-4000-8000-000000000123
29383
+ tracecli vulnerabilities get TRACE-123 --output json
29384
+ tracecli vulnerabilities get TRACE-123 --output tsv
29385
+ tracecli vulnerabilities get TRACE-123 --web`
29386
+ ).action(async (identifier, options) => {
29387
+ if (options.web) {
29388
+ const slug = await resolveOrganizationSlug();
29389
+ const url2 = `${getAppBaseUrl()}/dashboard/${slug}/vulnerabilities/${identifier}`;
29390
+ const opened = await openBrowser(url2);
29391
+ if (!opened) {
29392
+ console.log(`Open in browser: ${url2}`);
29393
+ }
29394
+ return;
29395
+ }
29396
+ const organizationId = await resolveOrganizationId();
29397
+ const result = await authenticatedRequest(
29398
+ (client) => client.vulnerability.getVulnerability({
29399
+ params: { organizationId, vulnerabilityId: identifier }
29400
+ })
29401
+ );
29402
+ if (result.status === 401) {
29403
+ console.error("Authentication failed. Run `tracecli auth login` to re-authenticate.");
29404
+ process.exit(1);
29405
+ }
29406
+ if (result.status === 400) {
29407
+ console.error(
29408
+ `Invalid vulnerability identifier '${identifier}'. Use TRACE-<number>, a positive integer, or a UUID.`
29409
+ );
29410
+ process.exit(1);
29411
+ }
29412
+ if (result.status === 404) {
29413
+ console.error(
29414
+ `Vulnerability '${identifier}' not found.
29415
+ Run \`tracecli vulnerabilities list\` to see available vulnerabilities.`
29416
+ );
29417
+ process.exit(1);
29418
+ }
29419
+ if (result.status !== 200) {
29420
+ console.error(`Failed to fetch vulnerability (status ${result.status}).`);
29421
+ process.exit(1);
29422
+ }
29423
+ if (options.output === "json") {
29424
+ console.log(JSON.stringify(result.body, null, 2));
29425
+ return;
29426
+ }
29427
+ if (options.output === "tsv") {
29428
+ printVulnerabilityDetailTsv(result.body);
29429
+ return;
29430
+ }
29431
+ printVulnerabilityDetail(result.body);
29432
+ });
29433
+ }
29122
29434
  function createListCommand3() {
29123
29435
  return new Command("list").description("List vulnerabilities for your organization").option("--repo <repo>", "Filter by repository name or owner/repo").option(
29124
29436
  "--severity <severity>",
@@ -29246,6 +29558,7 @@ Examples:
29246
29558
  }
29247
29559
  function createVulnerabilitiesCommand() {
29248
29560
  const vulnerabilities = new Command("vulnerabilities").description("Manage vulnerabilities");
29561
+ vulnerabilities.addCommand(createGetCommand());
29249
29562
  vulnerabilities.addCommand(createListCommand3());
29250
29563
  return vulnerabilities;
29251
29564
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securewithtrace",
3
- "version": "0.21.7",
3
+ "version": "0.23.0",
4
4
  "description": "Official Trace CLI for vulnerability intelligence in your terminal",
5
5
  "type": "module",
6
6
  "bin": {