securewithtrace 0.22.0 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -5
- package/dist/index.js +511 -197
- 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
|
|
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
|
|
241
|
+
| `--web` | `-w` | Open the pentest detail page in a browser |
|
|
220
242
|
|
|
221
243
|
### Upgrades
|
|
222
244
|
|
package/dist/index.js
CHANGED
|
@@ -25063,7 +25063,7 @@ var WebhookEventTypeSchema = external_exports.enum([
|
|
|
25063
25063
|
]);
|
|
25064
25064
|
var WebhookEndpointStatusSchema = external_exports.enum(["enabled", "disabled"]);
|
|
25065
25065
|
var WebhookDeliveryStatusSchema = external_exports.enum(["succeeded", "failed"]);
|
|
25066
|
-
var WebhookDeliverySourceSchema = external_exports.enum(["test", "mock"]);
|
|
25066
|
+
var WebhookDeliverySourceSchema = external_exports.enum(["test", "mock", "event"]);
|
|
25067
25067
|
var WebhookEndpointSchema = external_exports.object({
|
|
25068
25068
|
webhookId: external_exports.uuid(),
|
|
25069
25069
|
organizationId: external_exports.uuid(),
|
|
@@ -26536,6 +26536,75 @@ function tsvSafe(value) {
|
|
|
26536
26536
|
return (value ?? "").replace(/[\t\r\n]/g, " ");
|
|
26537
26537
|
}
|
|
26538
26538
|
|
|
26539
|
+
// src/pager.ts
|
|
26540
|
+
function isInteractiveEnvironment() {
|
|
26541
|
+
if (process.env["NO_PAGER"] === "1" || process.env["TRACECLI_NO_PAGER"] === "1") {
|
|
26542
|
+
return false;
|
|
26543
|
+
}
|
|
26544
|
+
return Boolean(process.stdout.isTTY && process.stdin.isTTY);
|
|
26545
|
+
}
|
|
26546
|
+
function resolvePagerCommand() {
|
|
26547
|
+
const raw = process.env["PAGER"]?.trim();
|
|
26548
|
+
if (raw && raw.length > 0) {
|
|
26549
|
+
const parts = raw.split(/\s+/);
|
|
26550
|
+
const [command, ...args] = parts;
|
|
26551
|
+
if (command) return [command, ...args];
|
|
26552
|
+
}
|
|
26553
|
+
return ["less"];
|
|
26554
|
+
}
|
|
26555
|
+
function flattenItem(item, index, renderItem) {
|
|
26556
|
+
const rendered = renderItem(item, index);
|
|
26557
|
+
return typeof rendered === "string" ? [rendered] : rendered;
|
|
26558
|
+
}
|
|
26559
|
+
function printWithoutPager(options) {
|
|
26560
|
+
const { items, header, renderItem } = options;
|
|
26561
|
+
if (header !== void 0) {
|
|
26562
|
+
console.log(header);
|
|
26563
|
+
}
|
|
26564
|
+
items.forEach((item, index) => {
|
|
26565
|
+
for (const line of flattenItem(item, index, renderItem)) {
|
|
26566
|
+
console.log(line);
|
|
26567
|
+
}
|
|
26568
|
+
});
|
|
26569
|
+
}
|
|
26570
|
+
async function runInteractivePager(options) {
|
|
26571
|
+
const { items, header, renderItem, disabled } = options;
|
|
26572
|
+
if (disabled || items.length === 0 || !isInteractiveEnvironment()) {
|
|
26573
|
+
printWithoutPager({ items, header, renderItem });
|
|
26574
|
+
return;
|
|
26575
|
+
}
|
|
26576
|
+
const command = resolvePagerCommand();
|
|
26577
|
+
try {
|
|
26578
|
+
const proc = Bun.spawn(command, {
|
|
26579
|
+
stdin: "pipe",
|
|
26580
|
+
stdout: "inherit",
|
|
26581
|
+
stderr: "inherit",
|
|
26582
|
+
env: { LESS: "FRX", ...process.env }
|
|
26583
|
+
});
|
|
26584
|
+
const writer = proc.stdin;
|
|
26585
|
+
try {
|
|
26586
|
+
if (header !== void 0) {
|
|
26587
|
+
writer.write(`${header}
|
|
26588
|
+
`);
|
|
26589
|
+
}
|
|
26590
|
+
items.forEach((item, index) => {
|
|
26591
|
+
for (const line of flattenItem(item, index, renderItem)) {
|
|
26592
|
+
writer.write(`${line}
|
|
26593
|
+
`);
|
|
26594
|
+
}
|
|
26595
|
+
});
|
|
26596
|
+
} catch {
|
|
26597
|
+
}
|
|
26598
|
+
try {
|
|
26599
|
+
writer.end();
|
|
26600
|
+
} catch {
|
|
26601
|
+
}
|
|
26602
|
+
await proc.exited;
|
|
26603
|
+
} catch {
|
|
26604
|
+
printWithoutPager({ items, header, renderItem });
|
|
26605
|
+
}
|
|
26606
|
+
}
|
|
26607
|
+
|
|
26539
26608
|
// src/auth/commands.ts
|
|
26540
26609
|
function getClientId() {
|
|
26541
26610
|
const clientId = "client_01KEZ01W5Q3107GTBY51WQ67HT";
|
|
@@ -26547,6 +26616,44 @@ function getClientId() {
|
|
|
26547
26616
|
);
|
|
26548
26617
|
}
|
|
26549
26618
|
var SAFE_ORGANIZATION_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
26619
|
+
var ORG_FILTER_FIELDS = ["name", "slug"];
|
|
26620
|
+
function parseOrgFilter(expression) {
|
|
26621
|
+
const match = expression.match(/^([^=!:]+)(!=|!:|=|:)(.*)$/);
|
|
26622
|
+
if (!match || !match[1] || !match[2] || match[3] === void 0) {
|
|
26623
|
+
throw new Error(
|
|
26624
|
+
`Invalid filter expression: '${expression}'. Format: <field><operator><value> (e.g. name:acme)`
|
|
26625
|
+
);
|
|
26626
|
+
}
|
|
26627
|
+
const field = match[1].trim();
|
|
26628
|
+
const operator = match[2];
|
|
26629
|
+
const value = match[3].trim();
|
|
26630
|
+
if (!ORG_FILTER_FIELDS.includes(field)) {
|
|
26631
|
+
throw new Error(
|
|
26632
|
+
`Unknown filter field '${field}'. Supported fields: ${ORG_FILTER_FIELDS.join(", ")}`
|
|
26633
|
+
);
|
|
26634
|
+
}
|
|
26635
|
+
if (value.length === 0) {
|
|
26636
|
+
throw new Error(
|
|
26637
|
+
`Invalid filter expression: '${expression}'. Value cannot be empty after the operator.`
|
|
26638
|
+
);
|
|
26639
|
+
}
|
|
26640
|
+
return { field, operator, value };
|
|
26641
|
+
}
|
|
26642
|
+
function matchesOrgFilter(item, filter) {
|
|
26643
|
+
const raw = filter.field === "name" ? item.name : item.slug;
|
|
26644
|
+
const cellLower = raw.toLowerCase();
|
|
26645
|
+
const valueLower = filter.value.toLowerCase();
|
|
26646
|
+
switch (filter.operator) {
|
|
26647
|
+
case "=":
|
|
26648
|
+
return cellLower === valueLower;
|
|
26649
|
+
case "!=":
|
|
26650
|
+
return cellLower !== valueLower;
|
|
26651
|
+
case ":":
|
|
26652
|
+
return cellLower.includes(valueLower);
|
|
26653
|
+
case "!:":
|
|
26654
|
+
return !cellLower.includes(valueLower);
|
|
26655
|
+
}
|
|
26656
|
+
}
|
|
26550
26657
|
function sanitizeForOutput(value) {
|
|
26551
26658
|
return value.replace(/[\r\n\t]/g, " ").trim();
|
|
26552
26659
|
}
|
|
@@ -26796,12 +26903,25 @@ ${colorSuccess(`Authenticated as ${displayName} (${tokenResponse.user.email})`)}
|
|
|
26796
26903
|
});
|
|
26797
26904
|
}
|
|
26798
26905
|
function createLogoutCommand() {
|
|
26799
|
-
return new Command("logout").description("Log out of Trace").addHelpText(
|
|
26906
|
+
return new Command("logout").description("Log out of Trace").option("--no-confirm", "Skip confirmation prompt (for CI/scripts)").addHelpText(
|
|
26800
26907
|
"after",
|
|
26801
26908
|
`
|
|
26802
26909
|
Examples:
|
|
26803
|
-
tracecli auth logout
|
|
26804
|
-
|
|
26910
|
+
tracecli auth logout
|
|
26911
|
+
tracecli auth logout --no-confirm`
|
|
26912
|
+
).action(async (options) => {
|
|
26913
|
+
if (options.confirm && process.stdin.isTTY && process.stdout.isTTY) {
|
|
26914
|
+
const readline = createInterface2({ input: process.stdin, output: process.stdout });
|
|
26915
|
+
try {
|
|
26916
|
+
const answer = await readline.question("Are you sure you want to log out? [Y/n] ");
|
|
26917
|
+
if (answer.trim().toLowerCase() === "n" || answer.trim().toLowerCase() === "no") {
|
|
26918
|
+
console.log("Logout cancelled.");
|
|
26919
|
+
return;
|
|
26920
|
+
}
|
|
26921
|
+
} finally {
|
|
26922
|
+
readline.close();
|
|
26923
|
+
}
|
|
26924
|
+
}
|
|
26805
26925
|
const removed = removeToken();
|
|
26806
26926
|
if (removed) {
|
|
26807
26927
|
console.log(
|
|
@@ -26857,6 +26977,9 @@ Examples:
|
|
|
26857
26977
|
const info = buildAuthStatusInfo(token, refreshToken, selectedOrganizationId);
|
|
26858
26978
|
if (options.output === "json") {
|
|
26859
26979
|
console.log(JSON.stringify(info, null, 2));
|
|
26980
|
+
if (!info.loggedIn) {
|
|
26981
|
+
process.exit(1);
|
|
26982
|
+
}
|
|
26860
26983
|
return;
|
|
26861
26984
|
}
|
|
26862
26985
|
if (options.output === "tsv") {
|
|
@@ -26871,11 +26994,14 @@ Examples:
|
|
|
26871
26994
|
info.activeOrg ?? ""
|
|
26872
26995
|
].join(" ")
|
|
26873
26996
|
);
|
|
26997
|
+
if (!info.loggedIn) {
|
|
26998
|
+
process.exit(1);
|
|
26999
|
+
}
|
|
26874
27000
|
return;
|
|
26875
27001
|
}
|
|
26876
27002
|
if (!info.loggedIn) {
|
|
26877
|
-
console.
|
|
26878
|
-
|
|
27003
|
+
console.error(colorError("Not logged in. Run `tracecli auth login` to authenticate."));
|
|
27004
|
+
process.exit(1);
|
|
26879
27005
|
}
|
|
26880
27006
|
console.log("Logged in to Trace");
|
|
26881
27007
|
if (info.userId) {
|
|
@@ -26915,7 +27041,12 @@ function createOrgListCommand() {
|
|
|
26915
27041
|
throw new Error("--limit must be a positive integer");
|
|
26916
27042
|
}
|
|
26917
27043
|
return n;
|
|
26918
|
-
}).option(
|
|
27044
|
+
}).option(
|
|
27045
|
+
"-f, --filter <expression>",
|
|
27046
|
+
"Filter expression (repeatable). Fields: name, slug. Operators: = exact, != not exact, : contains, !: not contains",
|
|
27047
|
+
(val, prev) => [...prev, val],
|
|
27048
|
+
[]
|
|
27049
|
+
).option("-w, --web", "Open the organization settings in a browser").option("--no-pager", "Disable the interactive arrow-key pager for table output").addHelpText(
|
|
26919
27050
|
"after",
|
|
26920
27051
|
`
|
|
26921
27052
|
Examples:
|
|
@@ -26923,70 +27054,95 @@ Examples:
|
|
|
26923
27054
|
tracecli org list --output json
|
|
26924
27055
|
tracecli org list --output tsv | cut -f1
|
|
26925
27056
|
tracecli org list --limit 5
|
|
27057
|
+
tracecli org list --filter "name:acme"
|
|
27058
|
+
tracecli org list --filter "slug!=internal"
|
|
27059
|
+
tracecli org list --no-pager
|
|
26926
27060
|
tracecli org list --web`
|
|
26927
|
-
).action(
|
|
26928
|
-
|
|
26929
|
-
|
|
26930
|
-
|
|
26931
|
-
|
|
26932
|
-
|
|
26933
|
-
|
|
27061
|
+
).action(
|
|
27062
|
+
async (options) => {
|
|
27063
|
+
if (options.web) {
|
|
27064
|
+
const slug = await resolveOrganizationSlug();
|
|
27065
|
+
const url2 = `${getAppBaseUrl()}/dashboard/${slug}/settings`;
|
|
27066
|
+
const opened = await openBrowser(url2);
|
|
27067
|
+
if (!opened) {
|
|
27068
|
+
console.log(`Open in browser: ${url2}`);
|
|
27069
|
+
}
|
|
27070
|
+
return;
|
|
26934
27071
|
}
|
|
26935
|
-
|
|
26936
|
-
|
|
26937
|
-
|
|
26938
|
-
|
|
26939
|
-
|
|
26940
|
-
|
|
26941
|
-
|
|
26942
|
-
|
|
26943
|
-
}
|
|
26944
|
-
|
|
26945
|
-
|
|
26946
|
-
|
|
26947
|
-
|
|
26948
|
-
|
|
26949
|
-
|
|
26950
|
-
|
|
26951
|
-
|
|
26952
|
-
|
|
27072
|
+
const parsedFilters = [];
|
|
27073
|
+
for (const expr of options.filter) {
|
|
27074
|
+
try {
|
|
27075
|
+
parsedFilters.push(parseOrgFilter(expr));
|
|
27076
|
+
} catch (err) {
|
|
27077
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
27078
|
+
process.exit(1);
|
|
27079
|
+
}
|
|
27080
|
+
}
|
|
27081
|
+
const organizationContext = await fetchOrganizationContext();
|
|
27082
|
+
const allItems = organizationContext.organizations.map(
|
|
27083
|
+
(organization) => ({
|
|
27084
|
+
...organization,
|
|
27085
|
+
active: organization.organizationId === organizationContext.activeOrganization.organizationId,
|
|
27086
|
+
selected: organization.organizationId === organizationContext.selectedOrganizationId
|
|
27087
|
+
})
|
|
27088
|
+
);
|
|
27089
|
+
const filteredItems = allItems.filter(
|
|
27090
|
+
(item) => parsedFilters.every((f) => matchesOrgFilter(item, f))
|
|
27091
|
+
);
|
|
27092
|
+
const items = options.limit !== void 0 ? filteredItems.slice(0, options.limit) : filteredItems;
|
|
27093
|
+
if (options.output === "json") {
|
|
27094
|
+
console.log(JSON.stringify(items, null, 2));
|
|
27095
|
+
return;
|
|
27096
|
+
}
|
|
27097
|
+
if (options.output === "tsv") {
|
|
27098
|
+
console.log("name slug organizationId active");
|
|
27099
|
+
for (const item of items) {
|
|
27100
|
+
console.log(
|
|
27101
|
+
[
|
|
27102
|
+
tsvSafe(item.name),
|
|
27103
|
+
tsvSafe(item.slug),
|
|
27104
|
+
tsvSafe(item.organizationId),
|
|
27105
|
+
item.active ? "true" : "false"
|
|
27106
|
+
].join(" ")
|
|
27107
|
+
);
|
|
27108
|
+
}
|
|
27109
|
+
return;
|
|
27110
|
+
}
|
|
27111
|
+
const headers = ["", "Name", "Slug", "Org ID"];
|
|
27112
|
+
const rows = items.map((item) => {
|
|
27113
|
+
const labels = [item.active ? "active" : null, item.selected ? "selected" : null].filter(
|
|
27114
|
+
Boolean
|
|
27115
|
+
);
|
|
27116
|
+
const marker = item.active ? "*" : " ";
|
|
27117
|
+
const nameSuffix = labels.length > 0 ? ` [${labels.join(", ")}]` : "";
|
|
27118
|
+
return [
|
|
27119
|
+
marker,
|
|
27120
|
+
sanitizeForOutput(item.name) + nameSuffix,
|
|
27121
|
+
sanitizeForOutput(item.slug),
|
|
27122
|
+
item.organizationId
|
|
27123
|
+
];
|
|
27124
|
+
});
|
|
27125
|
+
const colWidths = headers.map(
|
|
27126
|
+
(h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length))
|
|
27127
|
+
);
|
|
27128
|
+
const pad = (s, w3) => s.padEnd(w3);
|
|
27129
|
+
const headerLine = headers.map((h, i) => pad(h, colWidths[i] ?? h.length)).join(" ");
|
|
27130
|
+
const rowLines = rows.map(
|
|
27131
|
+
(row) => row.map((cell, i) => pad(cell, colWidths[i] ?? 0)).join(" ")
|
|
27132
|
+
);
|
|
27133
|
+
await runInteractivePager({
|
|
27134
|
+
items,
|
|
27135
|
+
header: headerLine,
|
|
27136
|
+
renderItem: (_item, i) => [rowLines[i] ?? ""],
|
|
27137
|
+
disabled: options.pager === false
|
|
27138
|
+
});
|
|
27139
|
+
if (organizationContext.staleSelectionDetected) {
|
|
26953
27140
|
console.log(
|
|
26954
|
-
|
|
26955
|
-
tsvSafe(item.name),
|
|
26956
|
-
tsvSafe(item.slug),
|
|
26957
|
-
tsvSafe(item.organizationId),
|
|
26958
|
-
item.active ? "true" : "false"
|
|
26959
|
-
].join(" ")
|
|
27141
|
+
"\nYour previous organization selection is no longer available and was reset."
|
|
26960
27142
|
);
|
|
26961
27143
|
}
|
|
26962
|
-
return;
|
|
26963
|
-
}
|
|
26964
|
-
const headers = ["", "Name", "Slug", "Org ID"];
|
|
26965
|
-
const rows = items.map((item) => {
|
|
26966
|
-
const labels = [item.active ? "active" : null, item.selected ? "selected" : null].filter(
|
|
26967
|
-
Boolean
|
|
26968
|
-
);
|
|
26969
|
-
const marker = item.active ? "*" : " ";
|
|
26970
|
-
const nameSuffix = labels.length > 0 ? ` [${labels.join(", ")}]` : "";
|
|
26971
|
-
return [
|
|
26972
|
-
marker,
|
|
26973
|
-
sanitizeForOutput(item.name) + nameSuffix,
|
|
26974
|
-
sanitizeForOutput(item.slug),
|
|
26975
|
-
item.organizationId
|
|
26976
|
-
];
|
|
26977
|
-
});
|
|
26978
|
-
const colWidths = headers.map(
|
|
26979
|
-
(h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length))
|
|
26980
|
-
);
|
|
26981
|
-
const pad = (s, w3) => s.padEnd(w3);
|
|
26982
|
-
console.log(headers.map((h, i) => pad(h, colWidths[i] ?? h.length)).join(" "));
|
|
26983
|
-
for (const row of rows) {
|
|
26984
|
-
console.log(row.map((cell, i) => pad(cell, colWidths[i] ?? 0)).join(" "));
|
|
26985
27144
|
}
|
|
26986
|
-
|
|
26987
|
-
console.log("\nYour previous organization selection is no longer available and was reset.");
|
|
26988
|
-
}
|
|
26989
|
-
});
|
|
27145
|
+
);
|
|
26990
27146
|
}
|
|
26991
27147
|
function createOrgCurrentCommand() {
|
|
26992
27148
|
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(
|
|
@@ -27043,15 +27199,42 @@ Examples:
|
|
|
27043
27199
|
});
|
|
27044
27200
|
}
|
|
27045
27201
|
function createOrgSwitchCommand() {
|
|
27046
|
-
return new Command("switch").description("Switch the active organization for subsequent CLI commands").argument("[organization]", "Organization ID, slug, name, or menu selection index").addHelpText(
|
|
27202
|
+
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(
|
|
27047
27203
|
"after",
|
|
27048
27204
|
`
|
|
27049
27205
|
Examples:
|
|
27050
27206
|
tracecli org switch
|
|
27051
27207
|
tracecli org switch engineering
|
|
27052
|
-
tracecli org switch org_01KJTJFFG497Z1C54Z07AP6NA3
|
|
27053
|
-
|
|
27208
|
+
tracecli org switch org_01KJTJFFG497Z1C54Z07AP6NA3
|
|
27209
|
+
tracecli org switch engineering --web`
|
|
27210
|
+
).action(async (organization, options) => {
|
|
27054
27211
|
const normalizedOrganizationInput = organization?.trim() ?? "";
|
|
27212
|
+
if (options.web) {
|
|
27213
|
+
if (normalizedOrganizationInput.length > 0) {
|
|
27214
|
+
const organizationContext2 = await fetchOrganizationContext();
|
|
27215
|
+
const selection = resolveOrganizationSelectionInput(
|
|
27216
|
+
organizationContext2.organizations,
|
|
27217
|
+
normalizedOrganizationInput
|
|
27218
|
+
);
|
|
27219
|
+
if ("error" in selection) {
|
|
27220
|
+
console.error(selection.error);
|
|
27221
|
+
process.exit(1);
|
|
27222
|
+
}
|
|
27223
|
+
const url2 = `${getAppBaseUrl()}/dashboard/${selection.organization.slug}/settings`;
|
|
27224
|
+
const opened = await openBrowser(url2);
|
|
27225
|
+
if (!opened) {
|
|
27226
|
+
console.log(`Open in browser: ${url2}`);
|
|
27227
|
+
}
|
|
27228
|
+
} else {
|
|
27229
|
+
const slug = await resolveOrganizationSlug();
|
|
27230
|
+
const url2 = `${getAppBaseUrl()}/dashboard/${slug}/settings`;
|
|
27231
|
+
const opened = await openBrowser(url2);
|
|
27232
|
+
if (!opened) {
|
|
27233
|
+
console.log(`Open in browser: ${url2}`);
|
|
27234
|
+
}
|
|
27235
|
+
}
|
|
27236
|
+
return;
|
|
27237
|
+
}
|
|
27055
27238
|
if (normalizedOrganizationInput.length === 0 && (!process.stdin.isTTY || !process.stdout.isTTY)) {
|
|
27056
27239
|
console.error(
|
|
27057
27240
|
"Missing required argument 'organization'. Run `tracecli org switch <organization>` in non-interactive environments."
|
|
@@ -27610,7 +27793,7 @@ function createListCommand() {
|
|
|
27610
27793
|
}
|
|
27611
27794
|
return Math.min(n, MAX_LIMIT);
|
|
27612
27795
|
}
|
|
27613
|
-
).option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").option("-w, --web", "Open the pentests dashboard in a browser").addHelpText(
|
|
27796
|
+
).option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").option("-w, --web", "Open the pentests dashboard in a browser").option("--no-pager", "Disable the interactive arrow-key pager for table output").addHelpText(
|
|
27614
27797
|
"after",
|
|
27615
27798
|
`
|
|
27616
27799
|
Examples:
|
|
@@ -27619,84 +27802,95 @@ Examples:
|
|
|
27619
27802
|
tracecli pentest list --limit 10
|
|
27620
27803
|
tracecli pentest list --output json | jq '.[].title'
|
|
27621
27804
|
tracecli pentest list --output tsv | cut -f1,2
|
|
27805
|
+
tracecli pentest list --no-pager
|
|
27622
27806
|
tracecli pentest list --web`
|
|
27623
|
-
).action(
|
|
27624
|
-
|
|
27625
|
-
|
|
27626
|
-
|
|
27627
|
-
|
|
27628
|
-
|
|
27629
|
-
|
|
27807
|
+
).action(
|
|
27808
|
+
async (options) => {
|
|
27809
|
+
if (options.web) {
|
|
27810
|
+
const slug = await resolveOrganizationSlug();
|
|
27811
|
+
const url2 = `${getAppBaseUrl()}/dashboard/${slug}/pentests`;
|
|
27812
|
+
const opened = await openBrowser(url2);
|
|
27813
|
+
if (!opened) {
|
|
27814
|
+
console.log(`Open in browser: ${url2}`);
|
|
27815
|
+
}
|
|
27816
|
+
return;
|
|
27630
27817
|
}
|
|
27631
|
-
|
|
27632
|
-
|
|
27633
|
-
|
|
27634
|
-
|
|
27635
|
-
|
|
27636
|
-
|
|
27637
|
-
|
|
27638
|
-
|
|
27639
|
-
|
|
27640
|
-
console.error("Authentication failed. Run `tracecli auth login` to re-authenticate.");
|
|
27641
|
-
process.exit(1);
|
|
27642
|
-
}
|
|
27643
|
-
if (result.status !== 200) {
|
|
27644
|
-
console.error(`Failed to list pentests (status ${result.status}).`);
|
|
27645
|
-
process.exit(1);
|
|
27646
|
-
}
|
|
27647
|
-
let filteredPentests = result.body;
|
|
27648
|
-
if (options.status) {
|
|
27649
|
-
filteredPentests = filteredPentests.filter((p2) => p2.status === options.status);
|
|
27650
|
-
}
|
|
27651
|
-
const limit = options.limit ?? DEFAULT_LIMIT;
|
|
27652
|
-
const pentests = filteredPentests.slice(0, limit);
|
|
27653
|
-
const isJson = options.output === "json";
|
|
27654
|
-
const isTsv = options.output === "tsv";
|
|
27655
|
-
if (pentests.length === 0) {
|
|
27656
|
-
if (isJson) {
|
|
27657
|
-
console.log("[]");
|
|
27658
|
-
} else if (isTsv) {
|
|
27659
|
-
console.log("displayId title status startedAt endedAt findingsTotal");
|
|
27660
|
-
} else {
|
|
27661
|
-
console.log("No pentests found.");
|
|
27818
|
+
const organizationId = await resolveOrganizationId();
|
|
27819
|
+
const result = await authenticatedRequest(
|
|
27820
|
+
(client) => client.pentest.listPentests({
|
|
27821
|
+
params: { organizationId }
|
|
27822
|
+
})
|
|
27823
|
+
);
|
|
27824
|
+
if (result.status === 401) {
|
|
27825
|
+
console.error("Authentication failed. Run `tracecli auth login` to re-authenticate.");
|
|
27826
|
+
process.exit(1);
|
|
27662
27827
|
}
|
|
27663
|
-
|
|
27664
|
-
|
|
27665
|
-
|
|
27666
|
-
console.log(JSON.stringify(pentests, null, 2));
|
|
27667
|
-
return;
|
|
27668
|
-
}
|
|
27669
|
-
if (isTsv) {
|
|
27670
|
-
console.log("displayId title status startedAt endedAt findingsTotal");
|
|
27671
|
-
for (const pt of pentests) {
|
|
27672
|
-
const cols = [
|
|
27673
|
-
`PT-${pt.displayId}`,
|
|
27674
|
-
tsvSafe2(pt.title),
|
|
27675
|
-
pt.status,
|
|
27676
|
-
pt.startedAt ? String(pt.startedAt) : "",
|
|
27677
|
-
pt.endedAt ? String(pt.endedAt) : "",
|
|
27678
|
-
String(pt.findingCounts.total)
|
|
27679
|
-
];
|
|
27680
|
-
console.log(cols.join(" "));
|
|
27828
|
+
if (result.status !== 200) {
|
|
27829
|
+
console.error(`Failed to list pentests (status ${result.status}).`);
|
|
27830
|
+
process.exit(1);
|
|
27681
27831
|
}
|
|
27682
|
-
|
|
27683
|
-
|
|
27684
|
-
|
|
27685
|
-
`);
|
|
27686
|
-
for (const pt of pentests) {
|
|
27687
|
-
const status = formatStatus(pt.status);
|
|
27688
|
-
const started = formatDate(pt.startedAt);
|
|
27689
|
-
const ended = formatDate(pt.endedAt);
|
|
27690
|
-
const dateRange = pt.startedAt ? `${started} \u2013 ${ended}` : "Not started";
|
|
27691
|
-
const findings = pt.findingCounts.total;
|
|
27692
|
-
console.log(` [PT-${pt.displayId}] ${pt.title}`);
|
|
27693
|
-
console.log(` Status: ${status} | Findings: ${findings} | ${dateRange}`);
|
|
27694
|
-
if (pt.description) {
|
|
27695
|
-
console.log(` ${pt.description}`);
|
|
27832
|
+
let filteredPentests = result.body;
|
|
27833
|
+
if (options.status) {
|
|
27834
|
+
filteredPentests = filteredPentests.filter((p2) => p2.status === options.status);
|
|
27696
27835
|
}
|
|
27697
|
-
|
|
27836
|
+
const limit = options.limit ?? DEFAULT_LIMIT;
|
|
27837
|
+
const pentests = filteredPentests.slice(0, limit);
|
|
27838
|
+
const isJson = options.output === "json";
|
|
27839
|
+
const isTsv = options.output === "tsv";
|
|
27840
|
+
if (pentests.length === 0) {
|
|
27841
|
+
if (isJson) {
|
|
27842
|
+
console.log("[]");
|
|
27843
|
+
} else if (isTsv) {
|
|
27844
|
+
console.log("displayId title status startedAt endedAt findingsTotal");
|
|
27845
|
+
} else {
|
|
27846
|
+
console.log("No pentests found.");
|
|
27847
|
+
}
|
|
27848
|
+
return;
|
|
27849
|
+
}
|
|
27850
|
+
if (isJson) {
|
|
27851
|
+
console.log(JSON.stringify(pentests, null, 2));
|
|
27852
|
+
return;
|
|
27853
|
+
}
|
|
27854
|
+
if (isTsv) {
|
|
27855
|
+
console.log("displayId title status startedAt endedAt findingsTotal");
|
|
27856
|
+
for (const pt of pentests) {
|
|
27857
|
+
const cols = [
|
|
27858
|
+
`PT-${pt.displayId}`,
|
|
27859
|
+
tsvSafe2(pt.title),
|
|
27860
|
+
pt.status,
|
|
27861
|
+
pt.startedAt ? String(pt.startedAt) : "",
|
|
27862
|
+
pt.endedAt ? String(pt.endedAt) : "",
|
|
27863
|
+
String(pt.findingCounts.total)
|
|
27864
|
+
];
|
|
27865
|
+
console.log(cols.join(" "));
|
|
27866
|
+
}
|
|
27867
|
+
return;
|
|
27868
|
+
}
|
|
27869
|
+
const header = `Found ${pentests.length} pentest${pentests.length === 1 ? "" : "s"}:`;
|
|
27870
|
+
await runInteractivePager({
|
|
27871
|
+
items: pentests,
|
|
27872
|
+
header,
|
|
27873
|
+
renderItem: (pt) => renderPentestLines(pt),
|
|
27874
|
+
disabled: options.pager === false
|
|
27875
|
+
});
|
|
27698
27876
|
}
|
|
27699
|
-
|
|
27877
|
+
);
|
|
27878
|
+
}
|
|
27879
|
+
function renderPentestLines(pt) {
|
|
27880
|
+
const status = formatStatus(pt.status);
|
|
27881
|
+
const started = formatDate(pt.startedAt);
|
|
27882
|
+
const ended = formatDate(pt.endedAt);
|
|
27883
|
+
const dateRange = pt.startedAt ? `${started} \u2013 ${ended}` : "Not started";
|
|
27884
|
+
const findings = pt.findingCounts.total;
|
|
27885
|
+
const lines = [
|
|
27886
|
+
` [PT-${pt.displayId}] ${pt.title}`,
|
|
27887
|
+
` Status: ${status} | Findings: ${findings} | ${dateRange}`
|
|
27888
|
+
];
|
|
27889
|
+
if (pt.description) {
|
|
27890
|
+
lines.push(` ${pt.description}`);
|
|
27891
|
+
}
|
|
27892
|
+
lines.push("");
|
|
27893
|
+
return lines;
|
|
27700
27894
|
}
|
|
27701
27895
|
function createFindingsListCommand() {
|
|
27702
27896
|
return new Command("list").description("List findings for a pentest").requiredOption("--pentest <id>", "Pentest display ID (e.g. PT-3) or UUID").option(
|
|
@@ -27717,7 +27911,7 @@ function createFindingsListCommand() {
|
|
|
27717
27911
|
}
|
|
27718
27912
|
return Math.min(n, MAX_LIMIT);
|
|
27719
27913
|
}
|
|
27720
|
-
).option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").option("-w, --web", "Open the
|
|
27914
|
+
).option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").option("-w, --web", "Open the pentest detail page in a browser").option("--no-pager", "Disable the interactive arrow-key pager for table output").addHelpText(
|
|
27721
27915
|
"after",
|
|
27722
27916
|
`
|
|
27723
27917
|
Examples:
|
|
@@ -27726,12 +27920,15 @@ Examples:
|
|
|
27726
27920
|
tracecli pentest findings list --pentest PT-3 --status OPEN --limit 20
|
|
27727
27921
|
tracecli pentest findings list --pentest PT-3 --output json
|
|
27728
27922
|
tracecli pentest findings list --pentest PT-3 --output tsv | cut -f1,4
|
|
27923
|
+
tracecli pentest findings list --pentest PT-3 --no-pager
|
|
27729
27924
|
tracecli pentest findings list --pentest PT-3 --web`
|
|
27730
27925
|
).action(
|
|
27731
27926
|
async (options) => {
|
|
27732
27927
|
if (options.web) {
|
|
27733
27928
|
const slug = await resolveOrganizationSlug();
|
|
27734
|
-
const
|
|
27929
|
+
const numericMatch = options.pentest.match(/^(?:PT-)?(\d+)$/i);
|
|
27930
|
+
const pentestUrlId = numericMatch?.[1] ?? options.pentest;
|
|
27931
|
+
const url2 = `${getAppBaseUrl()}/dashboard/${slug}/pentests/${pentestUrlId}`;
|
|
27735
27932
|
const opened = await openBrowser(url2);
|
|
27736
27933
|
if (!opened) {
|
|
27737
27934
|
console.log(`Open in browser: ${url2}`);
|
|
@@ -27802,27 +27999,33 @@ Examples:
|
|
|
27802
27999
|
}
|
|
27803
28000
|
return;
|
|
27804
28001
|
}
|
|
27805
|
-
|
|
27806
|
-
|
|
27807
|
-
|
|
27808
|
-
|
|
27809
|
-
|
|
27810
|
-
|
|
27811
|
-
|
|
27812
|
-
const score = finding.cvssScore != null ? ` | CVSS ${finding.cvssScore.toFixed(1)}` : "";
|
|
27813
|
-
console.log(` [${severityLabel}] ${finding.title}`);
|
|
27814
|
-
console.log(` ${finding.category} \xB7 ${status}${cwes}${score}`);
|
|
27815
|
-
if (finding.repositories.length > 0) {
|
|
27816
|
-
for (const repo of finding.repositories) {
|
|
27817
|
-
const location = repo.filePath ? `${repo.repositoryName} \xB7 ${repo.filePath}${repo.startLine != null ? `:${repo.startLine}` : ""}` : repo.repositoryName;
|
|
27818
|
-
console.log(` ${location}`);
|
|
27819
|
-
}
|
|
27820
|
-
}
|
|
27821
|
-
console.log("");
|
|
27822
|
-
}
|
|
28002
|
+
const header = `Found ${findings.length} finding${findings.length === 1 ? "" : "s"}:`;
|
|
28003
|
+
await runInteractivePager({
|
|
28004
|
+
items: findings,
|
|
28005
|
+
header,
|
|
28006
|
+
renderItem: (finding) => renderPentestFindingLines(finding),
|
|
28007
|
+
disabled: options.pager === false
|
|
28008
|
+
});
|
|
27823
28009
|
}
|
|
27824
28010
|
);
|
|
27825
28011
|
}
|
|
28012
|
+
function renderPentestFindingLines(finding) {
|
|
28013
|
+
const sev = formatFindingSeverity(finding.severity);
|
|
28014
|
+
const severityLabel = colorSeverity(finding.severity, sev);
|
|
28015
|
+
const status = formatStatus(finding.status);
|
|
28016
|
+
const cwes = finding.cweIds.length > 0 ? ` | ${finding.cweIds.join(", ")}` : "";
|
|
28017
|
+
const score = finding.cvssScore != null ? ` | CVSS ${finding.cvssScore.toFixed(1)}` : "";
|
|
28018
|
+
const lines = [
|
|
28019
|
+
` [${severityLabel}] ${finding.title}`,
|
|
28020
|
+
` ${finding.category} \xB7 ${status}${cwes}${score}`
|
|
28021
|
+
];
|
|
28022
|
+
for (const repo of finding.repositories) {
|
|
28023
|
+
const location = repo.filePath ? `${repo.repositoryName} \xB7 ${repo.filePath}${repo.startLine != null ? `:${repo.startLine}` : ""}` : repo.repositoryName;
|
|
28024
|
+
lines.push(` ${location}`);
|
|
28025
|
+
}
|
|
28026
|
+
lines.push("");
|
|
28027
|
+
return lines;
|
|
28028
|
+
}
|
|
27826
28029
|
function createFindingsCommand() {
|
|
27827
28030
|
const findings = new Command("findings").description("Manage pentest findings");
|
|
27828
28031
|
findings.addCommand(createFindingsListCommand());
|
|
@@ -27885,6 +28088,7 @@ function extractRepoName(input) {
|
|
|
27885
28088
|
var BRANCH_NAME_PATTERN = /^[a-zA-Z0-9._/+-]+$/;
|
|
27886
28089
|
var TRACE_GIT_ASKPASS_TOKEN_ENV = "TRACE_GIT_ASKPASS_TOKEN";
|
|
27887
28090
|
var DEFAULT_LIMIT2 = 30;
|
|
28091
|
+
var DEFAULT_PAGE = 1;
|
|
27888
28092
|
var MAX_LIMIT2 = 100;
|
|
27889
28093
|
var REPOSITORY_PLATFORMS = ["GITHUB", "GITLAB", "BITBUCKET"];
|
|
27890
28094
|
var REPOSITORY_STATUSES = ["CONNECTED", "DISCONNECTED"];
|
|
@@ -28102,6 +28306,44 @@ function parseListLimit(value) {
|
|
|
28102
28306
|
}
|
|
28103
28307
|
return n;
|
|
28104
28308
|
}
|
|
28309
|
+
function parseListPage(value) {
|
|
28310
|
+
const n = Number(value);
|
|
28311
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
28312
|
+
throw new Error("--page must be a positive integer");
|
|
28313
|
+
}
|
|
28314
|
+
return n;
|
|
28315
|
+
}
|
|
28316
|
+
function paginateResults(items, options) {
|
|
28317
|
+
if (options.all) {
|
|
28318
|
+
return {
|
|
28319
|
+
items: [...items],
|
|
28320
|
+
pagination: null
|
|
28321
|
+
};
|
|
28322
|
+
}
|
|
28323
|
+
const limit = options.limit ?? DEFAULT_LIMIT2;
|
|
28324
|
+
const page = options.page ?? DEFAULT_PAGE;
|
|
28325
|
+
const totalItems = items.length;
|
|
28326
|
+
const totalPages = Math.max(1, Math.ceil(totalItems / limit));
|
|
28327
|
+
if (page > totalPages) {
|
|
28328
|
+
throw new Error(
|
|
28329
|
+
`--page ${page} is out of range for ${totalItems} matching results with --limit ${limit}. Use --page between 1 and ${totalPages}, or pass --all.`
|
|
28330
|
+
);
|
|
28331
|
+
}
|
|
28332
|
+
const startIndex = (page - 1) * limit;
|
|
28333
|
+
const endIndex = Math.min(startIndex + limit, totalItems);
|
|
28334
|
+
return {
|
|
28335
|
+
items: items.slice(startIndex, endIndex),
|
|
28336
|
+
pagination: {
|
|
28337
|
+
totalItems,
|
|
28338
|
+
totalPages,
|
|
28339
|
+
page,
|
|
28340
|
+
hasNextPage: page < totalPages,
|
|
28341
|
+
hasPreviousPage: page > 1,
|
|
28342
|
+
startIndex,
|
|
28343
|
+
endIndex
|
|
28344
|
+
}
|
|
28345
|
+
};
|
|
28346
|
+
}
|
|
28105
28347
|
function printRepositoriesTsv(repositories) {
|
|
28106
28348
|
console.log("repositoryUrl platform status defaultBranch language");
|
|
28107
28349
|
for (const repo of repositories) {
|
|
@@ -28116,7 +28358,7 @@ function printRepositoriesTsv(repositories) {
|
|
|
28116
28358
|
);
|
|
28117
28359
|
}
|
|
28118
28360
|
}
|
|
28119
|
-
function
|
|
28361
|
+
function buildRepositoriesTable(repositories) {
|
|
28120
28362
|
const headers = ["Repository URL", "Platform", "Status"];
|
|
28121
28363
|
const rows = repositories.map(
|
|
28122
28364
|
(repo) => [
|
|
@@ -28129,15 +28371,27 @@ function printRepositoriesTable(repositories) {
|
|
|
28129
28371
|
(h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length))
|
|
28130
28372
|
);
|
|
28131
28373
|
const pad = (s, w3) => s.padEnd(w3);
|
|
28132
|
-
|
|
28133
|
-
|
|
28134
|
-
|
|
28135
|
-
}
|
|
28374
|
+
const headerLine = headers.map((h, i) => pad(h, colWidths[i] ?? h.length)).join(" ");
|
|
28375
|
+
const rowLines = rows.map((row) => row.map((cell, i) => pad(cell, colWidths[i] ?? 0)).join(" "));
|
|
28376
|
+
return { headerLine, rowLines };
|
|
28136
28377
|
}
|
|
28137
28378
|
function collectFilterExpression(value, previous) {
|
|
28138
28379
|
previous.push(value);
|
|
28139
28380
|
return previous;
|
|
28140
28381
|
}
|
|
28382
|
+
function printPaginationHint(pagination, noun, nextPageFilterHint) {
|
|
28383
|
+
if (!pagination || pagination.totalPages <= 1) {
|
|
28384
|
+
return;
|
|
28385
|
+
}
|
|
28386
|
+
const shownStart = pagination.startIndex + 1;
|
|
28387
|
+
const shownEnd = pagination.endIndex;
|
|
28388
|
+
console.log(
|
|
28389
|
+
`Showing ${shownStart}-${shownEnd} of ${pagination.totalItems} ${noun} (page ${pagination.page}/${pagination.totalPages}).`
|
|
28390
|
+
);
|
|
28391
|
+
if (pagination.hasNextPage) {
|
|
28392
|
+
console.log(`Use --page ${pagination.page + 1}${nextPageFilterHint} (or --all) to see more.`);
|
|
28393
|
+
}
|
|
28394
|
+
}
|
|
28141
28395
|
function extractErrorMessage(body) {
|
|
28142
28396
|
if (typeof body === "object" && body !== null && "message" in body && typeof body.message === "string") {
|
|
28143
28397
|
const message = body.message.trim();
|
|
@@ -28263,9 +28517,14 @@ function addRepositoryFilterOptions(command) {
|
|
|
28263
28517
|
parseListLimit,
|
|
28264
28518
|
DEFAULT_LIMIT2
|
|
28265
28519
|
).option(
|
|
28520
|
+
"--page <n>",
|
|
28521
|
+
`Page number to display when results exceed --limit (default ${DEFAULT_PAGE})`,
|
|
28522
|
+
parseListPage,
|
|
28523
|
+
DEFAULT_PAGE
|
|
28524
|
+
).option("--all", "Display all matching repositories (disables pagination)").option(
|
|
28266
28525
|
"--dependency <pkg>",
|
|
28267
28526
|
"Filter repositories that use a dependency (e.g. lodash, lodash@4.17.21, @types/node@20)"
|
|
28268
|
-
).option("-w, --web", "Open the repositories dashboard in a browser");
|
|
28527
|
+
).option("-w, --web", "Open the repositories dashboard in a browser").option("--no-pager", "Disable the interactive arrow-key pager for table output");
|
|
28269
28528
|
}
|
|
28270
28529
|
async function runRepositoryListCommand(options) {
|
|
28271
28530
|
if (options.web) {
|
|
@@ -28314,8 +28573,17 @@ async function runRepositoryListCommand(options) {
|
|
|
28314
28573
|
console.log("No repositories found.");
|
|
28315
28574
|
return;
|
|
28316
28575
|
}
|
|
28317
|
-
|
|
28318
|
-
|
|
28576
|
+
let displayedRepositories;
|
|
28577
|
+
let pagination = null;
|
|
28578
|
+
try {
|
|
28579
|
+
const paginatedResult = paginateResults(filteredRepositories, options);
|
|
28580
|
+
displayedRepositories = paginatedResult.items;
|
|
28581
|
+
pagination = paginatedResult.pagination;
|
|
28582
|
+
} catch (error48) {
|
|
28583
|
+
const message = error48 instanceof Error ? error48.message : "Unknown pagination error.";
|
|
28584
|
+
console.error(`Invalid pagination options: ${message}`);
|
|
28585
|
+
process.exit(1);
|
|
28586
|
+
}
|
|
28319
28587
|
if (options.output === "json") {
|
|
28320
28588
|
console.log(JSON.stringify(displayedRepositories, null, 2));
|
|
28321
28589
|
return;
|
|
@@ -28324,7 +28592,18 @@ async function runRepositoryListCommand(options) {
|
|
|
28324
28592
|
printRepositoriesTsv(displayedRepositories);
|
|
28325
28593
|
return;
|
|
28326
28594
|
}
|
|
28327
|
-
|
|
28595
|
+
const { headerLine, rowLines } = buildRepositoriesTable(displayedRepositories);
|
|
28596
|
+
await runInteractivePager({
|
|
28597
|
+
items: displayedRepositories,
|
|
28598
|
+
header: headerLine,
|
|
28599
|
+
renderItem: (_repo, i) => [rowLines[i] ?? ""],
|
|
28600
|
+
disabled: options.pager === false
|
|
28601
|
+
});
|
|
28602
|
+
printPaginationHint(
|
|
28603
|
+
pagination,
|
|
28604
|
+
"repositories",
|
|
28605
|
+
options.filter.length > 0 ? " with the same --filter flags" : ""
|
|
28606
|
+
);
|
|
28328
28607
|
}
|
|
28329
28608
|
async function runDependencyFilter(organizationId, options) {
|
|
28330
28609
|
const dependency = options.dependency;
|
|
@@ -28378,8 +28657,17 @@ async function runDependencyFilter(organizationId, options) {
|
|
|
28378
28657
|
console.log(`No repositories found using dependency '${label}'.`);
|
|
28379
28658
|
return;
|
|
28380
28659
|
}
|
|
28381
|
-
|
|
28382
|
-
|
|
28660
|
+
let displayedRepos;
|
|
28661
|
+
let pagination = null;
|
|
28662
|
+
try {
|
|
28663
|
+
const paginatedResult = paginateResults(repos, options);
|
|
28664
|
+
displayedRepos = paginatedResult.items;
|
|
28665
|
+
pagination = paginatedResult.pagination;
|
|
28666
|
+
} catch (error48) {
|
|
28667
|
+
const message = error48 instanceof Error ? error48.message : "Unknown pagination error.";
|
|
28668
|
+
console.error(`Invalid pagination options: ${message}`);
|
|
28669
|
+
process.exit(1);
|
|
28670
|
+
}
|
|
28383
28671
|
if (options.output === "json") {
|
|
28384
28672
|
console.log(JSON.stringify(displayedRepos, null, 2));
|
|
28385
28673
|
return;
|
|
@@ -28391,10 +28679,12 @@ async function runDependencyFilter(organizationId, options) {
|
|
|
28391
28679
|
}
|
|
28392
28680
|
return;
|
|
28393
28681
|
}
|
|
28394
|
-
|
|
28395
|
-
|
|
28396
|
-
|
|
28397
|
-
|
|
28682
|
+
await runInteractivePager({
|
|
28683
|
+
items: displayedRepos,
|
|
28684
|
+
renderItem: (repo) => [`${repo.fullName} (${repo.versions.join(", ")})`],
|
|
28685
|
+
disabled: options.pager === false
|
|
28686
|
+
});
|
|
28687
|
+
printPaginationHint(pagination, "repositories", "");
|
|
28398
28688
|
}
|
|
28399
28689
|
function createListCommand2() {
|
|
28400
28690
|
return addRepositoryFilterOptions(
|
|
@@ -28405,6 +28695,8 @@ function createListCommand2() {
|
|
|
28405
28695
|
Examples:
|
|
28406
28696
|
tracecli repo list
|
|
28407
28697
|
tracecli repo list --platform GITHUB --status CONNECTED
|
|
28698
|
+
tracecli repo list --page 2 --limit 30
|
|
28699
|
+
tracecli repo list --all
|
|
28408
28700
|
tracecli repo list --output json | jq '.[].repositoryUrl'
|
|
28409
28701
|
tracecli repo list --output tsv | cut -f1
|
|
28410
28702
|
tracecli repo list --filter "language:typescript" --limit 10
|
|
@@ -28422,7 +28714,11 @@ function createFilterCommand() {
|
|
|
28422
28714
|
`
|
|
28423
28715
|
Examples:
|
|
28424
28716
|
tracecli repo filter --filter "platform=GITHUB" --filter "status=CONNECTED"
|
|
28425
|
-
tracecli repo filter --
|
|
28717
|
+
tracecli repo filter --page 2 --limit 30
|
|
28718
|
+
tracecli repo filter --all
|
|
28719
|
+
tracecli repo filter --language typescript --limit 10
|
|
28720
|
+
tracecli repo filter --output json | jq '.[].repositoryUrl'
|
|
28721
|
+
tracecli repo filter --output tsv | cut -f1
|
|
28426
28722
|
tracecli repo filter --web`
|
|
28427
28723
|
).action(async (options) => {
|
|
28428
28724
|
await runRepositoryListCommand(options);
|
|
@@ -29093,22 +29389,24 @@ function formatSeverity(severity) {
|
|
|
29093
29389
|
return severity;
|
|
29094
29390
|
}
|
|
29095
29391
|
}
|
|
29096
|
-
function
|
|
29097
|
-
const
|
|
29392
|
+
function renderVulnerabilityLines(vuln) {
|
|
29393
|
+
const sev = formatSeverity(vuln.severity);
|
|
29394
|
+
const displaySeverity = colorSeverity(vuln.severity, sev);
|
|
29395
|
+
const status = formatStatus(vuln.status);
|
|
29396
|
+
const repo = vuln.repositoryName;
|
|
29397
|
+
const file2 = vuln.codeSnippet.filePath;
|
|
29398
|
+
const line = vuln.codeSnippet.startLine;
|
|
29399
|
+
return [
|
|
29400
|
+
` [${displaySeverity}] ${vuln.title}`,
|
|
29401
|
+
` ${repo} \xB7 ${file2}:${line} \xB7 ${status}`,
|
|
29402
|
+
` ${vuln.traceId}`,
|
|
29403
|
+
""
|
|
29404
|
+
];
|
|
29405
|
+
}
|
|
29406
|
+
function sortVulnerabilitiesBySeverity(vulnerabilities) {
|
|
29407
|
+
return [...vulnerabilities].sort(
|
|
29098
29408
|
(a, b) => (SEVERITY_ORDER[a.severity] ?? 99) - (SEVERITY_ORDER[b.severity] ?? 99)
|
|
29099
29409
|
);
|
|
29100
|
-
for (const vuln of sorted) {
|
|
29101
|
-
const sev = formatSeverity(vuln.severity);
|
|
29102
|
-
const displaySeverity = colorSeverity(vuln.severity, sev);
|
|
29103
|
-
const status = formatStatus(vuln.status);
|
|
29104
|
-
const repo = vuln.repositoryName;
|
|
29105
|
-
const file2 = vuln.codeSnippet.filePath;
|
|
29106
|
-
const line = vuln.codeSnippet.startLine;
|
|
29107
|
-
console.log(` [${displaySeverity}] ${vuln.title}`);
|
|
29108
|
-
console.log(` ${repo} \xB7 ${file2}:${line} \xB7 ${status}`);
|
|
29109
|
-
console.log(` ${vuln.traceId}`);
|
|
29110
|
-
console.log("");
|
|
29111
|
-
}
|
|
29112
29410
|
}
|
|
29113
29411
|
function sanitizeTsvField(value) {
|
|
29114
29412
|
return value.replace(/[\t\n\r]/g, " ");
|
|
@@ -29177,7 +29475,7 @@ function printVulnerabilityDetailTsv(vulnerability) {
|
|
|
29177
29475
|
console.log(cols.join(" "));
|
|
29178
29476
|
}
|
|
29179
29477
|
function createGetCommand() {
|
|
29180
|
-
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").addHelpText(
|
|
29478
|
+
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(
|
|
29181
29479
|
"after",
|
|
29182
29480
|
`
|
|
29183
29481
|
Examples:
|
|
@@ -29185,8 +29483,18 @@ Examples:
|
|
|
29185
29483
|
tracecli vulnerabilities get 123
|
|
29186
29484
|
tracecli vulnerabilities get 00000000-0000-4000-8000-000000000123
|
|
29187
29485
|
tracecli vulnerabilities get TRACE-123 --output json
|
|
29188
|
-
tracecli vulnerabilities get TRACE-123 --output tsv
|
|
29486
|
+
tracecli vulnerabilities get TRACE-123 --output tsv
|
|
29487
|
+
tracecli vulnerabilities get TRACE-123 --web`
|
|
29189
29488
|
).action(async (identifier, options) => {
|
|
29489
|
+
if (options.web) {
|
|
29490
|
+
const slug = await resolveOrganizationSlug();
|
|
29491
|
+
const url2 = `${getAppBaseUrl()}/dashboard/${slug}/vulnerabilities/${identifier}`;
|
|
29492
|
+
const opened = await openBrowser(url2);
|
|
29493
|
+
if (!opened) {
|
|
29494
|
+
console.log(`Open in browser: ${url2}`);
|
|
29495
|
+
}
|
|
29496
|
+
return;
|
|
29497
|
+
}
|
|
29190
29498
|
const organizationId = await resolveOrganizationId();
|
|
29191
29499
|
const result = await authenticatedRequest(
|
|
29192
29500
|
(client) => client.vulnerability.getVulnerability({
|
|
@@ -29249,7 +29557,7 @@ function createListCommand3() {
|
|
|
29249
29557
|
"Additional filter expression: field=value or field!=value. Repeatable. Supported fields: vulnerabilityType, cweId, scanMode (and any other API-supported field).",
|
|
29250
29558
|
(value, previous) => [...previous, value],
|
|
29251
29559
|
[]
|
|
29252
|
-
).option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").option("-w, --web", "Open the vulnerabilities dashboard in a browser").addHelpText(
|
|
29560
|
+
).option("-o, --output <format>", "Output format: table, json, tsv (default: table)", "table").option("-w, --web", "Open the vulnerabilities dashboard in a browser").option("--no-pager", "Disable the interactive arrow-key pager for table output").addHelpText(
|
|
29253
29561
|
"after",
|
|
29254
29562
|
`
|
|
29255
29563
|
Examples:
|
|
@@ -29260,6 +29568,7 @@ Examples:
|
|
|
29260
29568
|
tracecli vulnerabilities list --filter vulnerabilityType=SAST --filter scanMode=sast
|
|
29261
29569
|
tracecli vulnerabilities list --output json | jq '.[].title'
|
|
29262
29570
|
tracecli vulnerabilities list --output tsv | cut -f1,7
|
|
29571
|
+
tracecli vulnerabilities list --no-pager
|
|
29263
29572
|
tracecli vulnerabilities list --web`
|
|
29264
29573
|
).action(
|
|
29265
29574
|
async (options) => {
|
|
@@ -29342,10 +29651,15 @@ Examples:
|
|
|
29342
29651
|
} else if (isTsv) {
|
|
29343
29652
|
printVulnerabilitiesTsv(items);
|
|
29344
29653
|
} else {
|
|
29654
|
+
const sorted = sortVulnerabilitiesBySeverity(items);
|
|
29345
29655
|
const total = collected.length;
|
|
29346
|
-
|
|
29347
|
-
|
|
29348
|
-
|
|
29656
|
+
const header = `Found ${total} vulnerabilit${total === 1 ? "y" : "ies"}:`;
|
|
29657
|
+
await runInteractivePager({
|
|
29658
|
+
items: sorted,
|
|
29659
|
+
header,
|
|
29660
|
+
renderItem: (vuln) => renderVulnerabilityLines(vuln),
|
|
29661
|
+
disabled: options.pager === false
|
|
29662
|
+
});
|
|
29349
29663
|
}
|
|
29350
29664
|
}
|
|
29351
29665
|
);
|