@cantinasecurity/apex-cli 0.1.28 → 0.1.34
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/.claude/skills/apex-cli/SKILL.md +18 -1
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.mcp.claude.json +1 -1
- package/.mcp.codex.json +1 -1
- package/MARKETPLACE.md +1 -1
- package/README.md +51 -12
- package/dist/apex.js +41 -8
- package/dist/args.js +17 -1
- package/dist/auth.js +2 -2
- package/dist/commands.js +64 -11
- package/dist/findings.js +15 -3
- package/dist/help.js +53 -6
- package/dist/mcp.js +92 -10
- package/dist/onboarding-error-redaction.js +27 -0
- package/dist/onboarding.js +18 -3
- package/dist/redaction.js +14 -7
- package/dist/scan.js +49 -7
- package/dist/service-keys.js +2 -1
- package/dist/shell.js +29 -6
- package/dist/telemetry.js +4 -2
- package/dist/threat-model.js +435 -0
- package/package.json +1 -1
- package/skills/apex-cli/SKILL.md +18 -1
package/dist/help.js
CHANGED
|
@@ -5,6 +5,11 @@ export const CLI_HELP_TEXT = `Usage:
|
|
|
5
5
|
apex scans List scans for the current workspace binding
|
|
6
6
|
apex findings List findings for the latest or selected scan
|
|
7
7
|
apex findings --workspace List findings across the workspace browser scan set
|
|
8
|
+
apex findings send <finding-ref> <linear|jira>
|
|
9
|
+
Create a ticket using the connected integration and saved destination
|
|
10
|
+
apex findings tickets <finding-ref> List linked Linear and Jira tickets
|
|
11
|
+
apex findings link-ticket <finding-ref> <linear|jira> <issue-key|url>
|
|
12
|
+
Link an existing ticket using a connected integration
|
|
8
13
|
apex findings comment <finding-id|finding-identifier>
|
|
9
14
|
Add a comment or note to a finding
|
|
10
15
|
apex findings feedback <finding-id|finding-identifier> <valid|invalid>
|
|
@@ -20,7 +25,7 @@ export const CLI_HELP_TEXT = `Usage:
|
|
|
20
25
|
apex status Show progress for the latest or selected scan
|
|
21
26
|
apex doctor Validate auth, repos, connections, and workspace binding
|
|
22
27
|
apex login Sign in to Apex
|
|
23
|
-
apex redeem <
|
|
28
|
+
apex redeem <code> Redeem a Standard-scan code without device login
|
|
24
29
|
apex claim Link a guest workspace and findings to a Cantina account
|
|
25
30
|
apex logout Sign out locally
|
|
26
31
|
apex service-key create Create an organization service key (shown once)
|
|
@@ -38,12 +43,12 @@ export const CLI_HELP_TEXT = `Usage:
|
|
|
38
43
|
Flags:
|
|
39
44
|
--company <id-or-handle> Override the automatically selected Apex company
|
|
40
45
|
--name <name> Name a new service key
|
|
41
|
-
--scope <scope[,scope]> Limit a service key to scans:create, scans:read, and/or findings:
|
|
46
|
+
--scope <scope[,scope]> Limit a service key to scans:create, scans:read, findings:read, and/or findings:write
|
|
42
47
|
--expires-at <ISO-8601> Set an optional service-key expiration time
|
|
43
48
|
--workspace-name <name> Set the Apex workspace name for this directory
|
|
44
49
|
--scan <scan-id> Select a specific scan for status, findings, or export
|
|
45
50
|
--workspace Use the cross-scan workspace findings view
|
|
46
|
-
--workspace-ref <name|prefix|id> Select a workspace for
|
|
51
|
+
--workspace-ref <name|prefix|id> Select a workspace for cross-scan findings
|
|
47
52
|
--finding-state all|open Select all findings or only findings needing review
|
|
48
53
|
--filter <[!]facet:value> Filter workspace findings; repeat for multiple filters
|
|
49
54
|
--status valid|invalid Set the finding feedback status explicitly
|
|
@@ -81,6 +86,7 @@ Tips:
|
|
|
81
86
|
audit is the current scan mode for audit scans; ultra remains accepted as a legacy alias.
|
|
82
87
|
Lite scans use --mode lite and require beta access plus provider-backed GitHub or GitLab sources.
|
|
83
88
|
apex workspace use accepts a workspace name, prefix, or ID.
|
|
89
|
+
Scan selectors use the public Bedrock scan ID shown by Apex; Kernel execution IDs are private and are not accepted as aliases.
|
|
84
90
|
Finding comments, feedback, and fix review scans use the same Apex login credentials as other CLI commands.
|
|
85
91
|
Trial codes go only to apex redeem. Never echo, log, save, telemeter, or send them through MCP.
|
|
86
92
|
Trial redemption, claim, and revocation require https://ai.cantina.xyz and reject other APEX_BASE_URL origins before sending secrets.
|
|
@@ -92,10 +98,15 @@ Tips:
|
|
|
92
98
|
Service-key commands require an existing login; run apex login first when needed.
|
|
93
99
|
APEX_SERVICE_KEY authenticates https://ai.cantina.xyz/mcp and hosted REST; it does not replace apex login for local CLI or stdio MCP commands.
|
|
94
100
|
Apex automatically selects your company when only one is available. With multiple companies, choose one interactively or pass --company.
|
|
95
|
-
Service-key creation also requires --name and reveals the secret only once; omit --scope to grant
|
|
101
|
+
Service-key creation also requires --name and reveals the secret only once; omit --scope to grant scans:create, scans:read, and findings:read only.
|
|
96
102
|
Service-key management is available only in the scripted CLI, not the interactive shell or MCP server.
|
|
97
103
|
Invalid finding feedback requires --dismissal-reason.
|
|
98
104
|
Fix review scans use saved or supplied Fix PR URLs when present; otherwise they review the current repository when Bedrock has original scan repository context.
|
|
105
|
+
Ticket linking verifies the company's connected integration, never creates a ticket, and never replaces a link.
|
|
106
|
+
Sending creates a ticket with the same title, details, and Apex link as the frontend, using its saved destination.
|
|
107
|
+
Configure Linear routing or the Jira workspace destination in Apex first. Already sent or linked findings return the existing ticket.
|
|
108
|
+
After an ambiguous send failure, check the provider and apex findings tickets before retrying.
|
|
109
|
+
Hosted MCP ticket creation and linking require an explicitly granted findings:write service-key scope; default scopes stay unchanged.
|
|
99
110
|
Finding identifiers such as KERN2-25 resolve against the selected scan; pass --scan or use the finding UUID directly when needed.
|
|
100
111
|
Use apex findings --workspace --company <handle> --workspace-ref <ref> --finding-state open to retrieve findings needing review without changing the local binding.
|
|
101
112
|
To run scans or exports against a different workspace, bind it first with apex workspace use <ref>.
|
|
@@ -105,6 +116,26 @@ Tips:
|
|
|
105
116
|
Quote workspace names that contain spaces:
|
|
106
117
|
apex workspace use "Core Platform"
|
|
107
118
|
`;
|
|
119
|
+
const AUDIT_SETUP_CLI_HELP = `
|
|
120
|
+
Audit Setup beta:
|
|
121
|
+
apex scan --workspace-ref <ref> --mode standard|audit --setup guided|auto
|
|
122
|
+
Prepare a threat model for an enabled workspace
|
|
123
|
+
apex threat-model <action> Read, answer, review, and approve a prepared model
|
|
124
|
+
--draft <uuid> Select an Audit Setup draft
|
|
125
|
+
--file <path|-> Read answer JSON, message, or revision text (stdin: -)
|
|
126
|
+
--attachment <path> Attach a file to an answer or message (repeatable)
|
|
127
|
+
--sha256 <hash> Select the exact reviewed model
|
|
128
|
+
--start-scan Confirm scan launch when approving a model
|
|
129
|
+
--request-id <uuid> Reuse an operation ID when retrying
|
|
130
|
+
--no-feedback-context Exclude prior feedback from this setup
|
|
131
|
+
--wait Wait for the next actionable setup state
|
|
132
|
+
--timeout <seconds> Bound next --wait (default 60, maximum 3600)
|
|
133
|
+
`;
|
|
134
|
+
export function cliHelpText(auditSetupEnabled) {
|
|
135
|
+
return auditSetupEnabled
|
|
136
|
+
? `${CLI_HELP_TEXT.trimEnd()}\n${AUDIT_SETUP_CLI_HELP}`
|
|
137
|
+
: CLI_HELP_TEXT;
|
|
138
|
+
}
|
|
108
139
|
export const SHELL_HELP_TEXT = `Press Tab to autocomplete commands and common arguments.
|
|
109
140
|
|
|
110
141
|
Commands:
|
|
@@ -116,6 +147,11 @@ Commands:
|
|
|
116
147
|
/findings [scan-id] List findings for the latest or selected scan
|
|
117
148
|
/findings workspace [filter...]
|
|
118
149
|
List workspace findings; for example review:unreviewed
|
|
150
|
+
/findings send <finding-ref> <linear|jira>
|
|
151
|
+
Create a ticket using the connected integration and saved destination
|
|
152
|
+
/findings tickets <finding-ref> List linked Linear and Jira tickets
|
|
153
|
+
/findings link-ticket <finding-ref> <linear|jira> <issue-key|url>
|
|
154
|
+
Link an existing ticket using a connected integration
|
|
119
155
|
/findings comment <finding-ref> <comment>
|
|
120
156
|
Add a comment or note to a finding
|
|
121
157
|
/findings feedback <finding-ref> valid [comment]
|
|
@@ -147,8 +183,8 @@ Commands:
|
|
|
147
183
|
/exit Exit Apex
|
|
148
184
|
|
|
149
185
|
Scripted-only commands (run outside the interactive shell):
|
|
150
|
-
apex redeem <
|
|
151
|
-
|
|
186
|
+
apex redeem <code> [--json] [--replace]
|
|
187
|
+
Redeem a Standard-scan code without device login or browser access
|
|
152
188
|
apex service-key create --name <name> [--company <id-or-handle>] [--scope <scope[,scope]>]
|
|
153
189
|
Create an organization service key (shown once)
|
|
154
190
|
apex service-key list [--company <id-or-handle>]
|
|
@@ -161,6 +197,7 @@ Tips:
|
|
|
161
197
|
audit is the current scan mode for audit scans; ultra remains accepted as a legacy alias.
|
|
162
198
|
Lite scans require beta access plus provider-backed GitHub or GitLab sources.
|
|
163
199
|
/workspace use accepts a workspace name, prefix, or ID.
|
|
200
|
+
Scan selectors use the public Bedrock scan ID shown by Apex; Kernel execution IDs are private and are not accepted as aliases.
|
|
164
201
|
/findings comment, /findings feedback, and /findings fix-review use the current Apex login.
|
|
165
202
|
For first-class open state or a workspace that is not locally bound, use scripted
|
|
166
203
|
apex findings --workspace --company <handle> --workspace-ref <ref> --finding-state open.
|
|
@@ -174,3 +211,13 @@ Tips:
|
|
|
174
211
|
Anonymous usage telemetry can be disabled with /telemetry disable or APEX_TELEMETRY_DISABLED=1.
|
|
175
212
|
Quote workspace names that contain spaces: /workspace use "Core Platform"
|
|
176
213
|
`;
|
|
214
|
+
const AUDIT_SETUP_SHELL_HELP = `
|
|
215
|
+
Audit Setup beta:
|
|
216
|
+
Start the shell with --setup guided|auto and --mode standard|audit to prepare
|
|
217
|
+
the active workspace's threat model before explicitly approving its scan.
|
|
218
|
+
`;
|
|
219
|
+
export function shellHelpText(auditSetupEnabled) {
|
|
220
|
+
return auditSetupEnabled
|
|
221
|
+
? `${SHELL_HELP_TEXT.trimEnd()}\n${AUDIT_SETUP_SHELL_HELP}`
|
|
222
|
+
: SHELL_HELP_TEXT;
|
|
223
|
+
}
|
package/dist/mcp.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { commandStartThreatModel, commandThreatModel, hasAuditSetupBeta } from "./threat-model.js";
|
|
1
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
4
|
import path from "node:path";
|
|
@@ -5,8 +6,8 @@ import { z } from "zod";
|
|
|
5
6
|
import { getMe, logout, readStableDeviceLoginState, startDeviceLogin, waitForDeviceLoginApproval, } from "./auth.js";
|
|
6
7
|
import { ApexApiClient, formatApiError } from "./api-client.js";
|
|
7
8
|
import { getClaimProofState, isClaimProofAvailable, } from "./claim-proof.js";
|
|
8
|
-
import { commandCancelScan, commandConnect, commandCredits, commandDoctor, commandExportFindings, commandFindingComment, commandFindingFeedback, commandFindingFixReview, commandFindings, commandWorkspaceFindings, commandScan, commandScans, commandStatus, commandWorkspace, commandWorkspaceUse, commandWorkspaces, } from "./commands.js";
|
|
9
|
-
import {
|
|
9
|
+
import { commandCancelScan, commandConnect, commandCredits, commandDoctor, commandExportFindings, commandFindingComment, commandFindingTickets, commandFindingLinkTicket, commandFindingSend, commandFindingFeedback, commandFindingFixReview, commandFindings, commandWorkspaceFindings, commandScan, commandScans, commandStatus, commandWorkspace, commandWorkspaceUse, commandWorkspaces, } from "./commands.js";
|
|
10
|
+
import { cliHelpText } from "./help.js";
|
|
10
11
|
import { getLocalTrialStatus, isProofOnlyOnboardingCredentials, revokeGuestOnboardingSession, } from "./onboarding.js";
|
|
11
12
|
import { createMcpServerTelemetryInvocation, createMcpTelemetryInvocation, emitInvocationCompleted, emitInvocationStarted, emitMcpServerStarted, withTelemetryContext, } from "./telemetry.js";
|
|
12
13
|
import { APEX_CLI_VERSION } from "./version.js";
|
|
@@ -21,8 +22,9 @@ Suggested workflow:
|
|
|
21
22
|
6. If a repository operation needs a different local binding, call apex-workspaces and apex-workspace-use.
|
|
22
23
|
7. Start and monitor scans with apex-scan, apex-status, apex-scans, apex-findings, and apex-export-findings. For cross-scan review queues, call apex-workspace-findings with company, workspaceRef, and findingState "open"; this direct selection does not require or change a local binding.
|
|
23
24
|
8. For PR scans, call apex-scan with mode "pr" and one or more pullRequests.
|
|
24
|
-
9.
|
|
25
|
-
10.
|
|
25
|
+
9. Send findings to Linear/Jira with apex-finding-send to create tickets using the frontend's saved destination. Link existing tickets with apex-finding-link-ticket and inspect links with apex-finding-tickets. The company integration must already be connected.
|
|
26
|
+
10. Leave finding comments or valid/invalid feedback with apex-finding-comment and apex-finding-feedback when review collaboration is needed.
|
|
27
|
+
11. To review a fix, call apex-finding-fix-review. Pass fixPrUrls for PR-based fix reviews, or omit them to review the current repository state when Bedrock has original scan repository context.
|
|
26
28
|
|
|
27
29
|
Notes:
|
|
28
30
|
- Pass cwd explicitly for repository-specific operations.
|
|
@@ -31,10 +33,20 @@ Notes:
|
|
|
31
33
|
- Organization service-key creation, listing, and revocation are intentionally scripted-CLI-only and are not exposed as MCP tools. Never ask a user to paste a service key into a prompt or tool argument. A hosted MCP client supplies it only as the HTTP Authorization header.
|
|
32
34
|
- Production agents that do not need local filesystem access can connect to https://ai.cantina.xyz/mcp over Streamable HTTP with Authorization: Bearer <service-key>. Serverless and cron applications can instead use the REST API documented at https://ai.cantina.xyz/docs/apex-api. This local stdio server continues to use device login.
|
|
33
35
|
- Use mode "audit" for audit scans; "ultra" remains accepted as a legacy alias. Use mode "lite" only for beta-enabled provider-backed Lite scans, and mode "pr" for GitHub pull request scans.
|
|
36
|
+
- Use only the public Bedrock scanId returned by Apex for scan inputs. Kernel execution IDs are private and are neither returned nor accepted as aliases.
|
|
34
37
|
- Starting a PR scan may run alongside active workspace scans and does not require force. Starting any non-PR scan still treats every active scan, including a PR scan, as a blocker; use force only when the user explicitly wants that overlap.
|
|
35
38
|
- Doctor is read-only for guest sessions and reports the local snapshot plan without reserving a workspace.
|
|
36
39
|
- Guest standard scans validate the complete local archive before resolving or reserving a workspace. For an unbound renamed checkout, Apex may recover only the sole empty guest workspace when no explicit workspace name was supplied.
|
|
37
40
|
- When terminal redemption returns JSON, parse stdout only and keep stderr warnings separate. An idempotent outcome resumes the existing guest session, which may already own a workspace.`;
|
|
41
|
+
const AUDIT_SETUP_MCP_GUIDE = `
|
|
42
|
+
|
|
43
|
+
Audit Setup beta:
|
|
44
|
+
- Use apex-scan-setup for a configured workspace, then apex-threat-model.
|
|
45
|
+
- Read the complete model and get explicit approval before approve with the reviewed sha256 and startScan true.
|
|
46
|
+
- These tools use the local device account. Hosted service-key MCP does not expose Audit Setup.`;
|
|
47
|
+
function mcpWorkflowGuide(auditSetupEnabled) {
|
|
48
|
+
return auditSetupEnabled ? `${MCP_WORKFLOW_GUIDE}${AUDIT_SETUP_MCP_GUIDE}` : MCP_WORKFLOW_GUIDE;
|
|
49
|
+
}
|
|
38
50
|
const WORKSPACE_BINDING_ERROR = "This directory is not bound to an Apex workspace yet. Run `apex workspaces` to list workspace names, prefixes, and IDs, then bind one with `apex workspace use \"<workspace name>\"`, or run `apex scan` to create or resolve one automatically.";
|
|
39
51
|
const MISSING_PROVIDER_CONNECTIONS_ERROR = "Missing provider connections. Re-run interactively or pre-connect providers.";
|
|
40
52
|
const PR_MISSING_PROVIDER_CONNECTIONS_ERROR_PREFIX = "PR scans require connected provider access for ";
|
|
@@ -190,7 +202,7 @@ async function requireAuthenticated(client) {
|
|
|
190
202
|
}
|
|
191
203
|
return me;
|
|
192
204
|
}
|
|
193
|
-
function registerResources(server) {
|
|
205
|
+
function registerResources(server, auditSetupEnabled = false) {
|
|
194
206
|
server.registerResource("apex-cli-help", "apex://help/cli", {
|
|
195
207
|
title: "Apex CLI Help",
|
|
196
208
|
description: "Command reference for the Apex CLI.",
|
|
@@ -199,7 +211,7 @@ function registerResources(server) {
|
|
|
199
211
|
contents: [
|
|
200
212
|
{
|
|
201
213
|
uri: uri.href,
|
|
202
|
-
text:
|
|
214
|
+
text: cliHelpText(auditSetupEnabled),
|
|
203
215
|
},
|
|
204
216
|
],
|
|
205
217
|
}));
|
|
@@ -211,12 +223,12 @@ function registerResources(server) {
|
|
|
211
223
|
contents: [
|
|
212
224
|
{
|
|
213
225
|
uri: uri.href,
|
|
214
|
-
text:
|
|
226
|
+
text: mcpWorkflowGuide(auditSetupEnabled),
|
|
215
227
|
},
|
|
216
228
|
],
|
|
217
229
|
}));
|
|
218
230
|
}
|
|
219
|
-
function registerTools(server) {
|
|
231
|
+
function registerTools(server, auditSetupEnabled = false) {
|
|
220
232
|
server.registerTool("apex-auth-status", {
|
|
221
233
|
title: "Apex Auth Status",
|
|
222
234
|
description: "Check whether Apex is already authenticated on this machine.",
|
|
@@ -494,6 +506,38 @@ function registerTools(server) {
|
|
|
494
506
|
company,
|
|
495
507
|
workspaceRef,
|
|
496
508
|
}));
|
|
509
|
+
if (auditSetupEnabled)
|
|
510
|
+
server.registerTool("apex-scan-setup", {
|
|
511
|
+
title: "Prepare a scan threat model",
|
|
512
|
+
description: "Use an existing configured workspace to prepare a Standard or Audit threat model. Preparation can use paid setup compute. Always pause for explicit user model approval before starting the analysis scan.",
|
|
513
|
+
inputSchema: { cwd: z.string().optional(), company: z.string().optional(), workspaceRef: z.string(), mode: z.enum(["standard", "audit"]), setup: z.enum(["guided", "auto"]), includeFeedbackContext: z.boolean().default(true), requestId: z.string().uuid().optional() },
|
|
514
|
+
}, async (input) => runTool("apex-scan-setup", async () => {
|
|
515
|
+
const flags = { json: true, "non-interactive": true, "workspace-ref": input.workspaceRef, mode: input.mode, setup: input.setup };
|
|
516
|
+
if (input.company)
|
|
517
|
+
flags.company = input.company;
|
|
518
|
+
if (input.requestId)
|
|
519
|
+
flags["request-id"] = input.requestId;
|
|
520
|
+
if (!input.includeFeedbackContext)
|
|
521
|
+
flags["no-feedback-context"] = true;
|
|
522
|
+
return commandStartThreatModel(new ApexApiClient(), resolveCwd(input.cwd), flags, false);
|
|
523
|
+
}, value => `Threat-model draft ${String(value.draftId)}: ${String(value.state)}.`, input));
|
|
524
|
+
if (auditSetupEnabled)
|
|
525
|
+
server.registerTool("apex-threat-model", {
|
|
526
|
+
title: "Read or respond to an Audit Setup draft",
|
|
527
|
+
description: "Read status or the full model, answer an exact pending question, submit context, request changes, or explicitly approve the reviewed SHA-256 and start its scan. Auto preparation is never approval. For answer, input is JSON containing questionEventId, questionAnswers and optional text/clientMessageId. For message and request-changes, input is plain text.",
|
|
528
|
+
inputSchema: { cwd: z.string().optional(), draftId: z.string().uuid(), action: z.enum(["status", "next", "show", "answer", "message", "request-changes", "approve"]), input: z.string().optional(), sha256: z.string().regex(/^[0-9a-f]{64}$/).optional(), startScan: z.boolean().optional(), requestId: z.string().uuid().optional(), wait: z.boolean().optional() },
|
|
529
|
+
}, async (input) => runTool("apex-threat-model", async () => {
|
|
530
|
+
const flags = { json: true, "non-interactive": true, draft: input.draftId };
|
|
531
|
+
if (input.sha256)
|
|
532
|
+
flags.sha256 = input.sha256;
|
|
533
|
+
if (input.startScan)
|
|
534
|
+
flags["start-scan"] = true;
|
|
535
|
+
if (input.requestId)
|
|
536
|
+
flags["request-id"] = input.requestId;
|
|
537
|
+
if (input.wait)
|
|
538
|
+
flags.wait = true;
|
|
539
|
+
return commandThreatModel(new ApexApiClient(), resolveCwd(input.cwd), flags, input.action, input.input, false);
|
|
540
|
+
}, value => `Threat-model draft ${String(value.draftId)}: ${String(value.state)}.`, input));
|
|
497
541
|
server.registerTool("apex-scan", {
|
|
498
542
|
title: "Start Apex Scan",
|
|
499
543
|
description: "Start a new Apex scan for the provided cwd by default. Pass repoPaths only to scan explicit alternate local roots. Use mode audit for audit scans, mode lite for beta-gated provider-backed Lite scans, and mode pr for GitHub pull request scans.",
|
|
@@ -649,6 +693,39 @@ function registerTools(server) {
|
|
|
649
693
|
...payload,
|
|
650
694
|
};
|
|
651
695
|
}, (value) => `Fetched Apex workspace findings for ${String(value.cwd)}.`, { cwd, company, workspaceRef, findingState, filters }));
|
|
696
|
+
server.registerTool("apex-finding-tickets", {
|
|
697
|
+
title: "List Apex Finding Tickets",
|
|
698
|
+
description: "List the Linear and Jira tickets linked to a finding. Uses the current Apex device login.",
|
|
699
|
+
inputSchema: { cwd: z.string().optional(), findingRef: z.string(), scanId: z.string().optional() },
|
|
700
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
701
|
+
}, async ({ cwd, findingRef, scanId }) => runTool("apex-finding-tickets", async () => {
|
|
702
|
+
const targetCwd = resolveCwd(cwd);
|
|
703
|
+
return { cwd: targetCwd, ...await commandFindingTickets(new ApexApiClient(), targetCwd, buildFlags({ scanId }), findingRef) };
|
|
704
|
+
}, () => "Loaded finding ticket links.", { cwd, findingRef, scanId }));
|
|
705
|
+
server.registerTool("apex-finding-link-ticket", {
|
|
706
|
+
title: "Link an Existing Ticket to an Apex Finding",
|
|
707
|
+
description: "Verify and link an existing Linear or Jira ticket using the company's connected integration. Accepts an issue key or HTTPS issue URL; does not create a ticket or replace a link. Uses the current Apex device login.",
|
|
708
|
+
inputSchema: {
|
|
709
|
+
cwd: z.string().optional(), findingRef: z.string(), scanId: z.string().optional(),
|
|
710
|
+
provider: z.enum(["linear", "jira"]), issueRef: z.string().trim().min(1).max(2048),
|
|
711
|
+
},
|
|
712
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
713
|
+
}, async ({ cwd, findingRef, scanId, provider, issueRef }) => runTool("apex-finding-link-ticket", async () => {
|
|
714
|
+
const targetCwd = resolveCwd(cwd);
|
|
715
|
+
return { cwd: targetCwd, ...await commandFindingLinkTicket(new ApexApiClient(), targetCwd, buildFlags({ scanId }), findingRef, provider, issueRef) };
|
|
716
|
+
}, () => "Linked existing ticket to finding.", { cwd, findingRef, scanId, provider, issueRef }));
|
|
717
|
+
server.registerTool("apex-finding-send", {
|
|
718
|
+
title: "Send an Apex Finding to Linear or Jira",
|
|
719
|
+
description: "Create a Linear or Jira ticket from a finding, using the connected company integration and the same saved destination, title, details, and Apex link as the frontend. Returns an existing ticket if already sent or linked. Configure the destination in Apex first. After a timeout, inspect ticket links and the provider before retrying. Uses device login.",
|
|
720
|
+
inputSchema: {
|
|
721
|
+
cwd: z.string().optional(), findingRef: z.string(), scanId: z.string().optional(),
|
|
722
|
+
provider: z.enum(["linear", "jira"]),
|
|
723
|
+
},
|
|
724
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
725
|
+
}, async ({ cwd, findingRef, scanId, provider }) => runTool("apex-finding-send", async () => {
|
|
726
|
+
const targetCwd = resolveCwd(cwd);
|
|
727
|
+
return { cwd: targetCwd, ...await commandFindingSend(new ApexApiClient(), targetCwd, buildFlags({ scanId }), findingRef, provider) };
|
|
728
|
+
}, () => "Sent finding to issue tracker.", { cwd, findingRef, scanId, provider }));
|
|
652
729
|
server.registerTool("apex-finding-comment", {
|
|
653
730
|
title: "Add Apex Finding Comment",
|
|
654
731
|
description: "Add a comment or note to an Apex finding using the current Apex login.",
|
|
@@ -818,8 +895,12 @@ export async function runMcpServer() {
|
|
|
818
895
|
version: APEX_CLI_VERSION,
|
|
819
896
|
});
|
|
820
897
|
const invocation = createMcpServerTelemetryInvocation();
|
|
821
|
-
|
|
822
|
-
|
|
898
|
+
const me = await getMe(new ApexApiClient(), {
|
|
899
|
+
signal: AbortSignal.timeout(1_500),
|
|
900
|
+
}).catch(() => null);
|
|
901
|
+
const auditSetupEnabled = hasAuditSetupBeta(me);
|
|
902
|
+
registerResources(server, auditSetupEnabled);
|
|
903
|
+
registerTools(server, auditSetupEnabled);
|
|
823
904
|
const transport = new StdioServerTransport();
|
|
824
905
|
emitMcpServerStarted(invocation);
|
|
825
906
|
await server.connect(transport);
|
|
@@ -831,5 +912,6 @@ export const testing = {
|
|
|
831
912
|
requireAuthenticated,
|
|
832
913
|
resolveCwd,
|
|
833
914
|
runTool,
|
|
915
|
+
mcpWorkflowGuide,
|
|
834
916
|
workflowGuide: MCP_WORKFLOW_GUIDE,
|
|
835
917
|
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { redactSensitiveText, redactSensitiveValue } from "./redaction.js";
|
|
2
|
+
// These top-level names describe the API error envelope, not user input.
|
|
3
|
+
const ONBOARDING_ERROR_FIELDS = new Set(["code", "error", "detail", "message"]);
|
|
4
|
+
// Public protocol enums are classification, never a free-form echo of the
|
|
5
|
+
// submitted code. Keep this allowlist aligned with Bedrock's onboarding errors.
|
|
6
|
+
const ONBOARDING_ERROR_CODES = new Set([
|
|
7
|
+
"INVALID_CODE_FORMAT", "INVALID_FINGERPRINT", "CODE_NOT_FOUND",
|
|
8
|
+
"CODE_EXPIRED", "CODE_REVOKED", "CODE_PAUSED", "CODE_EXHAUSTED",
|
|
9
|
+
"ACTIVE_GUEST_SESSION", "TRIAL_ALREADY_REDEEMED", "RATE_LIMITED",
|
|
10
|
+
"TRIAL_CAPACITY", "GUEST_SESSION_INVALID", "GUEST_SESSION_EXPIRED",
|
|
11
|
+
"GUEST_SESSION_CLAIMED", "GUEST_SESSION_REVOKED", "CLAIM_INVALID",
|
|
12
|
+
"CLAIM_EXPIRED", "ALREADY_CLAIMED", "FINGERPRINT_MISMATCH",
|
|
13
|
+
"CLAIM_PERSONAL_TENANT_EXISTS", "CLAIM_TARGET_UNAUTHORIZED",
|
|
14
|
+
"CLAIM_TARGET_PREFIX_EXHAUSTED", "CLAIM_SCAN_START_INDETERMINATE",
|
|
15
|
+
"CLAIM_RECONCILIATION_PENDING",
|
|
16
|
+
]);
|
|
17
|
+
export function redactOnboardingErrorBody(body, requestCode) {
|
|
18
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
19
|
+
return redactSensitiveValue(body, [requestCode]);
|
|
20
|
+
}
|
|
21
|
+
return Object.fromEntries(Object.entries(body).map(([key, value]) => [
|
|
22
|
+
ONBOARDING_ERROR_FIELDS.has(key) ? key : redactSensitiveText(key, [requestCode]),
|
|
23
|
+
key === "code" && typeof value === "string" && ONBOARDING_ERROR_CODES.has(value)
|
|
24
|
+
? value
|
|
25
|
+
: redactSensitiveValue(value, [requestCode]),
|
|
26
|
+
]));
|
|
27
|
+
}
|
package/dist/onboarding.js
CHANGED
|
@@ -3,9 +3,10 @@ import { loginWithoutPersist } from "./auth.js";
|
|
|
3
3
|
import { isClaimProofAvailable } from "./claim-proof.js";
|
|
4
4
|
import { ApiError, getApiErrorCode, redactSensitiveText, } from "./api-client.js";
|
|
5
5
|
import { clearCredentialsIfCurrent, clearGuestFieldsIfClaimTokenMatches, getOrCreateConfigString, loadConfig, loadCredentials, replaceCredentialsIfCurrent, updateConfig, withCredentialLifecycleLock, } from "./config.js";
|
|
6
|
+
import { redactOnboardingErrorBody } from "./onboarding-error-redaction.js";
|
|
6
7
|
import { APEX_CLI_VERSION } from "./version.js";
|
|
7
8
|
import { loadWorkspaceBinding, saveWorkspaceBinding } from "./workspace-binding.js";
|
|
8
|
-
const ONBOARDING_CODE_PATTERN = /^
|
|
9
|
+
const ONBOARDING_CODE_PATTERN = /^[A-Z0-9_-]{1,64}$/;
|
|
9
10
|
const ONBOARDING_FINGERPRINT_PATTERN = /^[a-f0-9]{64}$/;
|
|
10
11
|
const DEFAULT_CLIENT_NAME = "apex";
|
|
11
12
|
const HOSTED_APEX_ORIGIN = "https://ai.cantina.xyz";
|
|
@@ -541,8 +542,22 @@ async function redeemOnboardingCodeUnlocked(client, options = {}) {
|
|
|
541
542
|
: {}),
|
|
542
543
|
};
|
|
543
544
|
}
|
|
544
|
-
export function redeemOnboardingCode(client, options = {}) {
|
|
545
|
-
|
|
545
|
+
export async function redeemOnboardingCode(client, options = {}) {
|
|
546
|
+
const code = resolveOnboardingCode(options.code);
|
|
547
|
+
return withCredentialLifecycleLock(() => redeemOnboardingCodeUnlocked(client, { ...options, code })).catch((error) => {
|
|
548
|
+
// Custom codes have no recognizable prefix. Redact the exact request value
|
|
549
|
+
// at this boundary without retaining it in credentials or global state.
|
|
550
|
+
const message = redactSensitiveText(error instanceof Error ? error.message : String(error), [code]);
|
|
551
|
+
if (error instanceof ApiError) {
|
|
552
|
+
throw new ApiError(message, error.status, redactOnboardingErrorBody(error.body, code));
|
|
553
|
+
}
|
|
554
|
+
if (error instanceof AggregateError) {
|
|
555
|
+
throw new AggregateError(error.errors.map(e => new Error(redactSensitiveText(e instanceof Error ? e.message : String(e), [code]))), message);
|
|
556
|
+
}
|
|
557
|
+
// Rebuild ordinary errors so causes, stacks, and attached request fields
|
|
558
|
+
// cannot retain the custom code after its message has been sanitized.
|
|
559
|
+
throw new Error(message);
|
|
560
|
+
});
|
|
546
561
|
}
|
|
547
562
|
async function claimOnboardingSessionUnlocked(client, cwd, options = {}) {
|
|
548
563
|
let expectedCredentials = await loadCredentials();
|
package/dist/redaction.js
CHANGED
|
@@ -1,23 +1,30 @@
|
|
|
1
1
|
const APEX_SECRET_PATTERN = /(apex_(?:ct|at|rt|dc)_)[A-Za-z0-9._~+/=-]+/gi;
|
|
2
|
-
const ONBOARDING_CODE_PATTERN = /SPBT-[A-Z0-9]{2,24}-[A-Z0-9]{8}/gi;
|
|
2
|
+
const ONBOARDING_CODE_PATTERN = /(APEX|SPBT)-[A-Z0-9]{2,24}-[A-Z0-9]{8}/gi;
|
|
3
3
|
// Hex-only boundaries avoid redacting a 64-character window from a longer hex
|
|
4
4
|
// value while still catching fingerprints next to punctuation or prose.
|
|
5
5
|
const ONBOARDING_FINGERPRINT_PATTERN = /(?<![A-Fa-f0-9])[A-Fa-f0-9]{64}(?![A-Fa-f0-9])/g;
|
|
6
|
-
export function redactSensitiveText(value) {
|
|
7
|
-
|
|
6
|
+
export function redactSensitiveText(value, requestSecrets = []) {
|
|
7
|
+
let redacted = value;
|
|
8
|
+
for (const secret of requestSecrets) {
|
|
9
|
+
if (!secret)
|
|
10
|
+
continue;
|
|
11
|
+
const escaped = secret.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
12
|
+
redacted = redacted.replace(new RegExp(`(?<![A-Z0-9_-])${escaped}(?![A-Z0-9_-])`, "gi"), "[REDACTED]");
|
|
13
|
+
}
|
|
14
|
+
return redacted
|
|
8
15
|
.replace(APEX_SECRET_PATTERN, (_match, prefix) => `${prefix}[REDACTED]`)
|
|
9
|
-
.replace(ONBOARDING_CODE_PATTERN,
|
|
16
|
+
.replace(ONBOARDING_CODE_PATTERN, (_match, prefix) => `${prefix.toUpperCase()}-[REDACTED]`)
|
|
10
17
|
.replace(ONBOARDING_FINGERPRINT_PATTERN, "[REDACTED]");
|
|
11
18
|
}
|
|
12
19
|
export function containsSensitiveText(value) {
|
|
13
20
|
return redactSensitiveText(value) !== value;
|
|
14
21
|
}
|
|
15
22
|
/** Redacts every string key and value in an API error body without mutating it. */
|
|
16
|
-
export function redactSensitiveValue(value) {
|
|
23
|
+
export function redactSensitiveValue(value, requestSecrets = []) {
|
|
17
24
|
const seen = new WeakSet();
|
|
18
25
|
const visit = (current) => {
|
|
19
26
|
if (typeof current === "string")
|
|
20
|
-
return redactSensitiveText(current);
|
|
27
|
+
return redactSensitiveText(current, requestSecrets);
|
|
21
28
|
if (!current || typeof current !== "object")
|
|
22
29
|
return current;
|
|
23
30
|
if (seen.has(current))
|
|
@@ -26,7 +33,7 @@ export function redactSensitiveValue(value) {
|
|
|
26
33
|
if (Array.isArray(current))
|
|
27
34
|
return current.map(visit);
|
|
28
35
|
return Object.fromEntries(Object.entries(current).map(([key, nested]) => [
|
|
29
|
-
redactSensitiveText(key),
|
|
36
|
+
redactSensitiveText(key, requestSecrets),
|
|
30
37
|
visit(nested),
|
|
31
38
|
]));
|
|
32
39
|
};
|
package/dist/scan.js
CHANGED
|
@@ -11,6 +11,19 @@ const ACTIVE_SCAN_STATUSES = new Set([
|
|
|
11
11
|
function asRecord(value) {
|
|
12
12
|
return value && typeof value === "object" ? value : null;
|
|
13
13
|
}
|
|
14
|
+
const KERNEL_SCAN_IDENTIFIER_KEY = /^kernel_?scan_?id$/i;
|
|
15
|
+
export function stripKernelScanIdentifiers(value) {
|
|
16
|
+
if (Array.isArray(value)) {
|
|
17
|
+
return value.map(stripKernelScanIdentifiers);
|
|
18
|
+
}
|
|
19
|
+
const record = asRecord(value);
|
|
20
|
+
if (!record) {
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
return Object.fromEntries(Object.entries(record)
|
|
24
|
+
.filter(([key]) => !KERNEL_SCAN_IDENTIFIER_KEY.test(key))
|
|
25
|
+
.map(([key, entry]) => [key, stripKernelScanIdentifiers(entry)]));
|
|
26
|
+
}
|
|
14
27
|
function readString(...values) {
|
|
15
28
|
for (const value of values) {
|
|
16
29
|
if (typeof value === "string" && value.trim().length > 0) {
|
|
@@ -30,6 +43,14 @@ function readNumber(...values) {
|
|
|
30
43
|
}
|
|
31
44
|
return null;
|
|
32
45
|
}
|
|
46
|
+
function readBoolean(...values) {
|
|
47
|
+
for (const value of values) {
|
|
48
|
+
if (typeof value === "boolean") {
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
33
54
|
function readScanMode(...values) {
|
|
34
55
|
const mode = readString(...values);
|
|
35
56
|
return mode === "ultra" ? "audit" : mode;
|
|
@@ -55,13 +76,12 @@ function normalizeScanRecord(payload) {
|
|
|
55
76
|
if (!record) {
|
|
56
77
|
return null;
|
|
57
78
|
}
|
|
58
|
-
const scanId = readString(record.scanId, record.id, record.apexScanId, record.bedrockScanId
|
|
79
|
+
const scanId = readString(record.scanId, record.id, record.apexScanId, record.bedrockScanId);
|
|
59
80
|
if (!scanId) {
|
|
60
81
|
return null;
|
|
61
82
|
}
|
|
62
83
|
return {
|
|
63
84
|
scanId,
|
|
64
|
-
kernelScanId: readString(record.kernelScanId, record.kernel_scan_id),
|
|
65
85
|
apexScanId: readString(record.apexScanId, record.apex_scan_id),
|
|
66
86
|
bedrockScanId: readString(record.bedrockScanId, record.bedrock_scan_id),
|
|
67
87
|
displayName: readString(record.displayName, record.display_name, record.name),
|
|
@@ -75,6 +95,29 @@ function normalizeScanRecord(payload) {
|
|
|
75
95
|
finishedAt: readTimestamp(record, "finishedAt", "finished_at", "completedAt", "completed_at", "cancelledAt", "cancelled_at"),
|
|
76
96
|
};
|
|
77
97
|
}
|
|
98
|
+
export function normalizeScanStartResponse(payload) {
|
|
99
|
+
const record = asRecord(stripKernelScanIdentifiers(payload));
|
|
100
|
+
if (!record) {
|
|
101
|
+
throw new Error("Scan response was not a JSON object.");
|
|
102
|
+
}
|
|
103
|
+
const scanId = readString(record.apexScanId, record.apex_scan_id, record.bedrockScanId, record.bedrock_scan_id, record.scanId, record.scan_id);
|
|
104
|
+
const workspaceId = readString(record.workspaceId, record.workspace_id);
|
|
105
|
+
const apexScanId = readString(record.apexScanId, record.apex_scan_id);
|
|
106
|
+
const bedrockScanId = readString(record.bedrockScanId, record.bedrock_scan_id);
|
|
107
|
+
const scanUrl = readString(record.scanUrl, record.scan_url);
|
|
108
|
+
const fullScanRecommended = readBoolean(record.fullScanRecommended);
|
|
109
|
+
const fullScanRecommendation = readString(record.fullScanRecommendation);
|
|
110
|
+
return {
|
|
111
|
+
...(workspaceId ? { workspaceId } : {}),
|
|
112
|
+
...(scanId ? { scanId } : {}),
|
|
113
|
+
...(apexScanId ? { apexScanId } : {}),
|
|
114
|
+
...(bedrockScanId ? { bedrockScanId } : {}),
|
|
115
|
+
status: readString(record.status) ?? "unknown",
|
|
116
|
+
...(scanUrl ? { scanUrl } : {}),
|
|
117
|
+
...(fullScanRecommended !== null ? { fullScanRecommended } : {}),
|
|
118
|
+
...(fullScanRecommendation ? { fullScanRecommendation } : {}),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
78
121
|
async function requestWithFallbacks(client, attempts) {
|
|
79
122
|
let lastApiError = null;
|
|
80
123
|
for (const attempt of attempts) {
|
|
@@ -95,7 +138,7 @@ async function requestWithFallbacks(client, attempts) {
|
|
|
95
138
|
throw new Error("No scan routes were available.");
|
|
96
139
|
}
|
|
97
140
|
export function getTrackedScanId(scan) {
|
|
98
|
-
const trackedScanId = readString(scan.apexScanId, scan.bedrockScanId, scan.scanId
|
|
141
|
+
const trackedScanId = readString(scan.apexScanId, scan.bedrockScanId, scan.scanId);
|
|
99
142
|
if (!trackedScanId) {
|
|
100
143
|
throw new Error("Scan response did not include an Apex scan id.");
|
|
101
144
|
}
|
|
@@ -129,7 +172,6 @@ function mergeScanProgress(scan, progress) {
|
|
|
129
172
|
: scan.finishedAt;
|
|
130
173
|
return {
|
|
131
174
|
...scan,
|
|
132
|
-
kernelScanId: progress.kernelScanId ?? scan.kernelScanId,
|
|
133
175
|
status,
|
|
134
176
|
startedAt,
|
|
135
177
|
finishedAt,
|
|
@@ -152,7 +194,7 @@ export function findMostRelevantActiveScan(scans) {
|
|
|
152
194
|
return scans.find((scan) => isActiveScanStatus(scan.status)) ?? null;
|
|
153
195
|
}
|
|
154
196
|
export function matchesScanId(scan, scanId) {
|
|
155
|
-
return [scan.scanId, scan.apexScanId, scan.bedrockScanId
|
|
197
|
+
return [scan.scanId, scan.apexScanId, scan.bedrockScanId].includes(scanId);
|
|
156
198
|
}
|
|
157
199
|
export function selectDefaultScanToCancel(scans, lastScanId) {
|
|
158
200
|
const activeScans = scans.filter((scan) => isActiveScanStatus(scan.status));
|
|
@@ -179,7 +221,8 @@ export async function fetchWorkspaceScans(client, workspaceId) {
|
|
|
179
221
|
return Promise.all(scans.map((scan) => reconcileActiveScanStatus(client, scan)));
|
|
180
222
|
}
|
|
181
223
|
export async function fetchScanProgress(client, scanId) {
|
|
182
|
-
|
|
224
|
+
const response = await client.request(`/api/cli/v1/scans/${encodeURIComponent(scanId)}/progress`);
|
|
225
|
+
return stripKernelScanIdentifiers(response);
|
|
183
226
|
}
|
|
184
227
|
export async function cancelScan(client, scanId) {
|
|
185
228
|
const encodedScanId = encodeURIComponent(scanId);
|
|
@@ -205,7 +248,6 @@ export async function cancelScan(client, scanId) {
|
|
|
205
248
|
}
|
|
206
249
|
return {
|
|
207
250
|
scanId,
|
|
208
|
-
kernelScanId: null,
|
|
209
251
|
apexScanId: null,
|
|
210
252
|
bedrockScanId: null,
|
|
211
253
|
displayName: null,
|
package/dist/service-keys.js
CHANGED
|
@@ -2,6 +2,7 @@ export const SERVICE_API_SCOPES = [
|
|
|
2
2
|
"scans:create",
|
|
3
3
|
"scans:read",
|
|
4
4
|
"findings:read",
|
|
5
|
+
"findings:write",
|
|
5
6
|
];
|
|
6
7
|
export function isServiceApiScope(value) {
|
|
7
8
|
return (typeof value === "string" &&
|
|
@@ -13,7 +14,7 @@ function serviceApiKeysPath(companyId) {
|
|
|
13
14
|
function serviceApiKeyMetadata(key) {
|
|
14
15
|
const scopes = Array.isArray(key.scopes)
|
|
15
16
|
? [...new Set(key.scopes.filter(isServiceApiScope))]
|
|
16
|
-
: [
|
|
17
|
+
: ["scans:create", "scans:read", "findings:read"];
|
|
17
18
|
return {
|
|
18
19
|
id: key.id,
|
|
19
20
|
name: key.name,
|
package/dist/shell.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { commandStartThreatModel, hasAuditSetupBeta } from "./threat-model.js";
|
|
2
|
+
import { shellHelpText } from "./help.js";
|
|
3
|
+
import { commandCancelScan, commandClaim, commandConnect, commandCredits, commandDoctor, commandExportFindings, commandFindingComment, commandFindingTickets, commandFindingLinkTicket, commandFindingSend, commandFindingFeedback, commandFindingFixReview, commandFindings, commandWorkspaceFindings, commandLogout, commandScan, commandScans, commandStatus, commandTelemetry, commandUpdate, commandWorkspace, commandWorkspaceUse, commandWorkspaces, initializeInteractiveSession, openCurrentApexView, } from "./commands.js";
|
|
3
4
|
import { withFlag } from "./args.js";
|
|
4
5
|
import { printCompanyDetails, printInteractiveSessionSummary, printSourceList, printWorkspaceDetails, } from "./output.js";
|
|
5
6
|
import { readLine } from "./prompt.js";
|
|
@@ -12,7 +13,7 @@ const SHELL_COMPLETIONS = [
|
|
|
12
13
|
{ command: "scans" },
|
|
13
14
|
{
|
|
14
15
|
command: "findings",
|
|
15
|
-
args: ["workspace", "comment", "feedback", "fix-review"],
|
|
16
|
+
args: ["workspace", "comment", "feedback", "fix-review", "tickets", "link-ticket", "send"],
|
|
16
17
|
},
|
|
17
18
|
{ command: "export" },
|
|
18
19
|
{ command: "workspaces" },
|
|
@@ -133,8 +134,8 @@ export function getShellCompletionCandidates(input) {
|
|
|
133
134
|
function clearTerminal() {
|
|
134
135
|
process.stdout.write("\x1Bc");
|
|
135
136
|
}
|
|
136
|
-
function printShellHelp() {
|
|
137
|
-
process.stdout.write(`${
|
|
137
|
+
function printShellHelp(session) {
|
|
138
|
+
process.stdout.write(`${shellHelpText(hasAuditSetupBeta(session.me, session.company))}\n`);
|
|
138
139
|
}
|
|
139
140
|
export async function runInteractiveShell(client, cwd, initialFlags) {
|
|
140
141
|
let shellFlags = { ...initialFlags };
|
|
@@ -192,7 +193,7 @@ async function runShellCommandWithTelemetry(client, cwd, parsed, shellFlags, ses
|
|
|
192
193
|
async function runShellCommand(client, cwd, parsed, shellFlags, session) {
|
|
193
194
|
switch (parsed.command) {
|
|
194
195
|
case "help":
|
|
195
|
-
printShellHelp();
|
|
196
|
+
printShellHelp(session);
|
|
196
197
|
return {};
|
|
197
198
|
case "credits":
|
|
198
199
|
await commandCredits(client, cwd, shellFlags);
|
|
@@ -224,6 +225,10 @@ async function runShellCommand(client, cwd, parsed, shellFlags, session) {
|
|
|
224
225
|
: mode
|
|
225
226
|
? withFlag(shellFlags, "mode", mode)
|
|
226
227
|
: shellFlags;
|
|
228
|
+
if (commandFlags.setup !== undefined) {
|
|
229
|
+
await commandStartThreatModel(client, cwd, commandFlags);
|
|
230
|
+
return {};
|
|
231
|
+
}
|
|
227
232
|
const result = await commandScan(client, cwd, commandFlags);
|
|
228
233
|
return {
|
|
229
234
|
session: {
|
|
@@ -248,6 +253,24 @@ async function runShellCommand(client, cwd, parsed, shellFlags, session) {
|
|
|
248
253
|
await commandScans(client, cwd, shellFlags);
|
|
249
254
|
return {};
|
|
250
255
|
case "findings": {
|
|
256
|
+
if (parsed.args[0] === "send") {
|
|
257
|
+
if (parsed.args.length !== 3)
|
|
258
|
+
throw new Error("Usage: /findings send <finding-ref> <linear|jira>");
|
|
259
|
+
await commandFindingSend(client, cwd, shellFlags, parsed.args[1], parsed.args[2]);
|
|
260
|
+
return {};
|
|
261
|
+
}
|
|
262
|
+
if (parsed.args[0] === "tickets") {
|
|
263
|
+
if (parsed.args.length !== 2)
|
|
264
|
+
throw new Error("Usage: /findings tickets <finding-ref>");
|
|
265
|
+
await commandFindingTickets(client, cwd, shellFlags, parsed.args[1]);
|
|
266
|
+
return {};
|
|
267
|
+
}
|
|
268
|
+
if (parsed.args[0] === "link-ticket") {
|
|
269
|
+
if (parsed.args.length !== 4)
|
|
270
|
+
throw new Error("Usage: /findings link-ticket <finding-ref> <linear|jira> <issue-key|url>");
|
|
271
|
+
await commandFindingLinkTicket(client, cwd, shellFlags, parsed.args[1], parsed.args[2], parsed.args[3]);
|
|
272
|
+
return {};
|
|
273
|
+
}
|
|
251
274
|
if (parsed.args[0] === "workspace") {
|
|
252
275
|
await commandWorkspaceFindings(client, cwd, parsed.args.length > 1
|
|
253
276
|
? withFlag(shellFlags, "filter", parsed.args.slice(1))
|