@cantinasecurity/apex-cli 0.1.27 → 0.1.33
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 +25 -5
- 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 +90 -19
- 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 +51 -4
- package/dist/mcp.js +94 -12
- package/dist/scan.js +49 -7
- package/dist/service-key-environment.js +3 -3
- 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 +28 -5
package/dist/commands.js
CHANGED
|
@@ -4,12 +4,12 @@ import { ApiError, formatApiError, getApiErrorCode, } from "./api-client.js";
|
|
|
4
4
|
import { openInBrowser } from "./browser.js";
|
|
5
5
|
import { getClaimProofState, isClaimProofAvailable, parseClaimDeadlineMs, } from "./claim-proof.js";
|
|
6
6
|
import { loadConfig, updateConfig } from "./config.js";
|
|
7
|
-
import { createFindingComment, fetchScanExport, fetchScanFindings, fetchWorkspaceFindings, isFindingUuid, normalizeFindingRef, resolveFindingSelection, resolveScanSelection, startFindingFixReviewScan, submitFindingFeedback, } from "./findings.js";
|
|
7
|
+
import { createFindingComment, fetchFindingTickets, linkFindingTicket, sendFindingToIssueTracker, fetchScanExport, fetchScanFindings, fetchWorkspaceFindings, isFindingUuid, normalizeFindingRef, resolveFindingSelection, resolveScanSelection, startFindingFixReviewScan, submitFindingFeedback, } from "./findings.js";
|
|
8
8
|
import { prepareLocalSourceArchives, prepareExplicitScanSources, supportsLegacyRemoteFlow, } from "./local-source-scan.js";
|
|
9
9
|
import { logLine, printJson, withLoadingIndicator } from "./output.js";
|
|
10
10
|
import { claimOnboardingSession, isProofOnlyOnboardingCredentials, redeemOnboardingCode, revokeGuestOnboardingSession, } from "./onboarding.js";
|
|
11
11
|
import { confirm } from "./prompt.js";
|
|
12
|
-
import { cancelScan, fetchScanProgress, fetchWorkspaceScans, findMostRelevantActiveScan, getScanDisplayLabel, getScanDisplayId, getTrackedScanId, isActiveScanStatus, matchesScanId, selectDefaultScanToCancel, } from "./scan.js";
|
|
12
|
+
import { cancelScan, fetchScanProgress, fetchWorkspaceScans, findMostRelevantActiveScan, getScanDisplayLabel, getScanDisplayId, getTrackedScanId, isActiveScanStatus, matchesScanId, normalizeScanStartResponse, selectDefaultScanToCancel, } from "./scan.js";
|
|
13
13
|
import { createServiceApiKey, isServiceApiScope, listServiceApiKeys, revokeServiceApiKey, SERVICE_API_SCOPES, } from "./service-keys.js";
|
|
14
14
|
import { getServiceKeyCliAuthGuidance } from "./service-key-environment.js";
|
|
15
15
|
import { chooseCompany, createWorkspaceBinding, ensureAuthenticated, planGuestWorkspaceSelection, remediateMissingConnections, resolveWorkspaceSelection, resolveWorkspaceSelectionLegacy, resolveWorkspaceAllowingMissingConnections, selectWorkspaceTarget, } from "./session.js";
|
|
@@ -620,7 +620,6 @@ async function findFallbackActiveScan(client, binding) {
|
|
|
620
620
|
}
|
|
621
621
|
return {
|
|
622
622
|
scanId: progress.scanId,
|
|
623
|
-
kernelScanId: progress.kernelScanId,
|
|
624
623
|
apexScanId: binding.lastScanId,
|
|
625
624
|
bedrockScanId: null,
|
|
626
625
|
displayName: null,
|
|
@@ -1084,6 +1083,9 @@ export async function commandConnect(client, cwd, flags, provider) {
|
|
|
1084
1083
|
return { provider, company, url: response.url };
|
|
1085
1084
|
}
|
|
1086
1085
|
export async function commandScan(client, cwd, flags) {
|
|
1086
|
+
if (flags.setup !== undefined) {
|
|
1087
|
+
throw new Error("Audit Setup must use the threat-model preparation flow; a legacy scan was not started.");
|
|
1088
|
+
}
|
|
1087
1089
|
const requestedMode = normalizeRequestedScanMode(getFlagString(flags, "mode"));
|
|
1088
1090
|
const prSelections = parsePrScanPullRequestSelections(flags);
|
|
1089
1091
|
if (requestedMode !== "pr" && prSelections.length > 0) {
|
|
@@ -1159,7 +1161,8 @@ export async function commandScan(client, cwd, flags) {
|
|
|
1159
1161
|
const prSource = resolvePrScanSource(legacyResolve.repositories);
|
|
1160
1162
|
ensurePrSelectionsMatchSource(prSelections, prSource);
|
|
1161
1163
|
prScanSource = prSource;
|
|
1162
|
-
scan = await withLoadingIndicator("Starting PR scan...", flags, () => client
|
|
1164
|
+
scan = await withLoadingIndicator("Starting PR scan...", flags, () => client
|
|
1165
|
+
.request(`/api/cli/v1/local-workspaces/${encodeURIComponent(workspaceId)}/scan`, {
|
|
1163
1166
|
method: "POST",
|
|
1164
1167
|
json: {
|
|
1165
1168
|
mode: "pr",
|
|
@@ -1170,7 +1173,8 @@ export async function commandScan(client, cwd, flags) {
|
|
|
1170
1173
|
pullRequests: prSelections,
|
|
1171
1174
|
}),
|
|
1172
1175
|
},
|
|
1173
|
-
})
|
|
1176
|
+
})
|
|
1177
|
+
.then(normalizeScanStartResponse));
|
|
1174
1178
|
}
|
|
1175
1179
|
else if (requestedMode === "audit" || requestedMode === "lite") {
|
|
1176
1180
|
const modeLabel = requestedMode === "audit" ? "Audit" : "Lite";
|
|
@@ -1182,13 +1186,15 @@ export async function commandScan(client, cwd, flags) {
|
|
|
1182
1186
|
throw new Error("Workspace resolution did not return a workspaceId.");
|
|
1183
1187
|
}
|
|
1184
1188
|
workspaceId = legacyResolve.workspaceId;
|
|
1185
|
-
scan = await withLoadingIndicator(`Starting ${modeLabel.toLowerCase()} scan...`, flags, () => client
|
|
1189
|
+
scan = await withLoadingIndicator(`Starting ${modeLabel.toLowerCase()} scan...`, flags, () => client
|
|
1190
|
+
.request(`/api/cli/v1/local-workspaces/${encodeURIComponent(workspaceId)}/scan`, {
|
|
1186
1191
|
method: "POST",
|
|
1187
1192
|
json: {
|
|
1188
1193
|
mode: requestedMode,
|
|
1189
1194
|
force: forceRestart || flags.force === true,
|
|
1190
1195
|
},
|
|
1191
|
-
})
|
|
1196
|
+
})
|
|
1197
|
+
.then(normalizeScanStartResponse));
|
|
1192
1198
|
}
|
|
1193
1199
|
else {
|
|
1194
1200
|
try {
|
|
@@ -1200,7 +1206,7 @@ export async function commandScan(client, cwd, flags) {
|
|
|
1200
1206
|
detectedSources: result.sources,
|
|
1201
1207
|
preparedArchives,
|
|
1202
1208
|
});
|
|
1203
|
-
|
|
1209
|
+
const response = await client.request(`/api/cli/v2/local-workspaces/${encodeURIComponent(resolvedWorkspaceId)}/scan`, {
|
|
1204
1210
|
method: "POST",
|
|
1205
1211
|
json: {
|
|
1206
1212
|
mode: requestedMode,
|
|
@@ -1208,6 +1214,7 @@ export async function commandScan(client, cwd, flags) {
|
|
|
1208
1214
|
scanSources,
|
|
1209
1215
|
},
|
|
1210
1216
|
});
|
|
1217
|
+
return normalizeScanStartResponse(response);
|
|
1211
1218
|
});
|
|
1212
1219
|
}
|
|
1213
1220
|
finally {
|
|
@@ -1786,9 +1793,6 @@ export async function commandFindingFixReview(client, cwd, flags, requestedFindi
|
|
|
1786
1793
|
}
|
|
1787
1794
|
logLine(`Finding: ${selection.finding?.findingIdentifier ?? selection.findingId}`, flags);
|
|
1788
1795
|
logLine(`Fix review scan: ${response.fixReview.status} (${response.fixReview.scanId})`, flags);
|
|
1789
|
-
if (response.fixReview.kernelScanId) {
|
|
1790
|
-
logLine(`Kernel scan: ${response.fixReview.kernelScanId}`, flags);
|
|
1791
|
-
}
|
|
1792
1796
|
if (response.fixReview.reviewTarget) {
|
|
1793
1797
|
logLine(`Review target: ${formatFixReviewTarget(response.fixReview.reviewTarget)}`, flags);
|
|
1794
1798
|
}
|
|
@@ -1873,3 +1877,52 @@ export async function openCurrentApexView(client, cwd, flags) {
|
|
|
1873
1877
|
}
|
|
1874
1878
|
return url;
|
|
1875
1879
|
}
|
|
1880
|
+
async function resolveTicketFinding(client, cwd, flags, requestedFindingRef) {
|
|
1881
|
+
const findingRef = normalizeFindingRefInput(requestedFindingRef);
|
|
1882
|
+
const currentBinding = await loadWorkspaceBinding(cwd);
|
|
1883
|
+
const binding = isFindingUuid(findingRef)
|
|
1884
|
+
? currentBinding : requireLoadedWorkspaceBinding(currentBinding);
|
|
1885
|
+
if (!isFindingUuid(findingRef))
|
|
1886
|
+
await ensureAuthenticated(client, flags);
|
|
1887
|
+
return resolveFindingSelection({ client, binding, requestedFindingRef: findingRef,
|
|
1888
|
+
requestedScanId: getFlagString(flags, "scan") });
|
|
1889
|
+
}
|
|
1890
|
+
export async function commandFindingTickets(client, cwd, flags, findingRef) {
|
|
1891
|
+
const selection = await resolveTicketFinding(client, cwd, flags, findingRef);
|
|
1892
|
+
const result = await fetchFindingTickets(client, selection.findingId);
|
|
1893
|
+
const payload = { findingId: selection.findingId, findingRef: selection.findingRef, ...result };
|
|
1894
|
+
if (isJsonMode(flags))
|
|
1895
|
+
printJson(payload);
|
|
1896
|
+
else if (result.tickets.length === 0)
|
|
1897
|
+
logLine("No tickets linked to this finding.", flags);
|
|
1898
|
+
else
|
|
1899
|
+
for (const ticket of result.tickets)
|
|
1900
|
+
logLine(`${ticket.provider}: ${ticket.issueKey ?? ticket.issueId} ${ticket.issueUrl}`, flags);
|
|
1901
|
+
return payload;
|
|
1902
|
+
}
|
|
1903
|
+
export async function commandFindingLinkTicket(client, cwd, flags, findingRef, provider, issueRef) {
|
|
1904
|
+
if ((provider !== "linear" && provider !== "jira") || !issueRef.trim() || issueRef.trim().length > 2048) {
|
|
1905
|
+
throw new Error("Usage: apex findings link-ticket <finding-ref> <linear|jira> <issue-key|url>");
|
|
1906
|
+
}
|
|
1907
|
+
const selection = await resolveTicketFinding(client, cwd, flags, findingRef);
|
|
1908
|
+
const result = await linkFindingTicket(client, selection.findingId, { provider, issueRef: issueRef.trim() });
|
|
1909
|
+
const payload = { findingId: selection.findingId, findingRef: selection.findingRef, ...result };
|
|
1910
|
+
if (isJsonMode(flags))
|
|
1911
|
+
printJson(payload);
|
|
1912
|
+
else
|
|
1913
|
+
logLine(`${result.linked ? "Linked" : "Already linked"}: ${result.ticket.issueKey ?? result.ticket.issueId} ${result.ticket.issueUrl}`, flags);
|
|
1914
|
+
return payload;
|
|
1915
|
+
}
|
|
1916
|
+
export async function commandFindingSend(client, cwd, flags, findingRef, provider) {
|
|
1917
|
+
if (provider !== "linear" && provider !== "jira") {
|
|
1918
|
+
throw new Error("Usage: apex findings send <finding-ref> <linear|jira>");
|
|
1919
|
+
}
|
|
1920
|
+
const selection = await resolveTicketFinding(client, cwd, flags, findingRef);
|
|
1921
|
+
const result = await sendFindingToIssueTracker(client, selection.findingId, provider);
|
|
1922
|
+
const payload = { findingId: selection.findingId, findingRef: selection.findingRef, ...result };
|
|
1923
|
+
if (isJsonMode(flags))
|
|
1924
|
+
printJson(payload);
|
|
1925
|
+
else
|
|
1926
|
+
logLine(`${result.created ? "Created" : "Already sent"}: ${result.export.issueKey ?? result.export.issueId} ${result.export.issueUrl}`, flags);
|
|
1927
|
+
return payload;
|
|
1928
|
+
}
|
package/dist/findings.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { fetchWorkspaceScans, getScanDisplayId, matchesScanId, } from "./scan.js";
|
|
1
|
+
import { fetchWorkspaceScans, getScanDisplayId, matchesScanId, stripKernelScanIdentifiers, } from "./scan.js";
|
|
2
2
|
import { ApiError, CredentialGenerationChangedError, isTerminalGuestSessionError, } from "./api-client.js";
|
|
3
3
|
const FINDING_UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
4
4
|
const WORKSPACE_FINDING_FILTER_PATTERN = /^!?(severity|scan|repo|review|impact|likelihood|status|validation|visibility|duplicate|source):[^:]+$/;
|
|
5
5
|
const MAX_WORKSPACE_FINDING_FILTERS = 50;
|
|
6
6
|
const MAX_WORKSPACE_FINDING_FILTER_LENGTH = 128;
|
|
7
7
|
const CANTINA_AUTH_TOKEN_ENV = "CANTINA_AUTH_TOKEN";
|
|
8
|
-
const FINDING_WRITE_AUTH_ERROR = "Finding comments, feedback, and fix review scans require Apex authentication. Run `apex login` or use MCP `apex-auth-start` followed by `apex-auth-wait`, then retry.";
|
|
8
|
+
const FINDING_WRITE_AUTH_ERROR = "Finding comments, feedback, ticket links, ticket creation, and fix review scans require Apex authentication. Run `apex login` or use MCP `apex-auth-start` followed by `apex-auth-wait`, then retry.";
|
|
9
9
|
function parsePositiveInt(value) {
|
|
10
10
|
if (!value)
|
|
11
11
|
return null;
|
|
@@ -307,7 +307,7 @@ export async function startFindingFixReviewScan(client, findingId, input = {}) {
|
|
|
307
307
|
const fixPrUrls = input.fixPrUrls
|
|
308
308
|
?.map((url) => url.trim())
|
|
309
309
|
.filter((url) => url.length > 0) ?? [];
|
|
310
|
-
|
|
310
|
+
const response = await requestFindingAppRoute(client, `/api/findings/${encodeURIComponent(findingId)}/fix-review`, {
|
|
311
311
|
method: "POST",
|
|
312
312
|
json: {
|
|
313
313
|
...(input.reviewTarget ? { reviewTarget: input.reviewTarget } : {}),
|
|
@@ -316,4 +316,16 @@ export async function startFindingFixReviewScan(client, findingId, input = {}) {
|
|
|
316
316
|
: {}),
|
|
317
317
|
},
|
|
318
318
|
});
|
|
319
|
+
return stripKernelScanIdentifiers(response);
|
|
320
|
+
}
|
|
321
|
+
export async function fetchFindingTickets(client, findingId) {
|
|
322
|
+
return requestFindingAppRoute(client, `/api/findings/${encodeURIComponent(findingId)}/tickets`);
|
|
323
|
+
}
|
|
324
|
+
export async function linkFindingTicket(client, findingId, input) {
|
|
325
|
+
return requestFindingAppRoute(client, `/api/findings/${encodeURIComponent(findingId)}/tickets`, {
|
|
326
|
+
method: "POST", json: input,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
export async function sendFindingToIssueTracker(client, findingId, provider) {
|
|
330
|
+
return requestFindingAppRoute(client, `/api/findings/${encodeURIComponent(findingId)}/issue-trackers/${provider}`, { method: "POST" });
|
|
319
331
|
}
|
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>
|
|
@@ -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.
|
|
@@ -90,12 +96,17 @@ Tips:
|
|
|
90
96
|
After a guest scan, run apex claim on the same machine within the displayed claim deadline to keep the results.
|
|
91
97
|
Claim moves trial results into the selected Cantina workspace when you can run scans there, or into your existing personal workspace.
|
|
92
98
|
Service-key commands require an existing login; run apex login first when needed.
|
|
93
|
-
APEX_SERVICE_KEY authenticates
|
|
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]
|
|
@@ -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,20 +22,31 @@ 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.
|
|
29
31
|
- Tool calls are always non-interactive and never auto-open a browser.
|
|
30
32
|
- Finding write tools use the same Apex device-login credentials as read tools. Never ask for browser cookies or CANTINA_AUTH_TOKEN. An error requiring CANTINA_AUTH_TOKEN proves the MCP server is outdated; update Apex CLI, re-run apex setup for this client, restart it, and retry.
|
|
31
|
-
- 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
|
|
32
|
-
- HTTP-
|
|
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.
|
|
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
|
};
|
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,
|
|
@@ -14,9 +14,9 @@ export function getServiceKeyCliAuthGuidance(env = process.env) {
|
|
|
14
14
|
return null;
|
|
15
15
|
}
|
|
16
16
|
const legacyVariableGuidance = variableName === "APEX_API_KEY"
|
|
17
|
-
? " For REST
|
|
17
|
+
? " For hosted MCP and REST examples, rename APEX_API_KEY to APEX_SERVICE_KEY."
|
|
18
18
|
: "";
|
|
19
|
-
return (`Apex service key detected in ${variableName}, but service keys authenticate the hosted REST
|
|
20
|
-
"Use `apex login` for CLI device authentication, or use the REST API for a headless/service-account integration: https://ai.cantina.xyz/docs/apex-api." +
|
|
19
|
+
return (`Apex service key detected in ${variableName}, but service keys authenticate the hosted MCP and REST endpoints, not the local Apex CLI. ` +
|
|
20
|
+
"Use `apex login` for local CLI device authentication, or use https://ai.cantina.xyz/mcp or the REST API for a headless/service-account integration: https://ai.cantina.xyz/docs/apex-api." +
|
|
21
21
|
legacyVariableGuidance);
|
|
22
22
|
}
|