@cantinasecurity/apex-cli 0.1.36 → 0.1.38
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 +17 -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 +63 -13
- package/dist/apex.js +7 -1
- package/dist/api-client.js +19 -9
- package/dist/args.js +14 -0
- package/dist/commands.js +85 -39
- package/dist/findings.js +6 -1
- package/dist/help.js +15 -4
- package/dist/local-source-scan.js +25 -5
- package/dist/mcp.js +23 -4
- package/dist/repo-discovery.js +69 -16
- package/dist/session.js +24 -4
- package/dist/shell.js +15 -8
- package/dist/telemetry.js +2 -2
- package/dist/threat-model.js +21 -3
- package/dist/types.js +4 -1
- package/package.json +9 -8
- package/skills/apex-cli/SKILL.md +17 -1
package/dist/session.js
CHANGED
|
@@ -98,9 +98,16 @@ function selectBindingForWorkspaceName(flags, binding, workspaceName) {
|
|
|
98
98
|
}
|
|
99
99
|
function getSourceMode(flags) {
|
|
100
100
|
const value = getFlagString(flags, "source-mode");
|
|
101
|
-
|
|
101
|
+
if (value === null || value === "auto")
|
|
102
|
+
return "auto";
|
|
103
|
+
if (value === "remote" || value === "local")
|
|
104
|
+
return value;
|
|
105
|
+
throw new Error("--source-mode must be auto, remote, or local.");
|
|
102
106
|
}
|
|
103
107
|
function buildFallbackLocalArchivePlan(source, sourceMode) {
|
|
108
|
+
if (!source.absolutePath) {
|
|
109
|
+
throw new Error("A remote repository URL cannot use a local snapshot. Connect its provider or clone it locally, then retry.");
|
|
110
|
+
}
|
|
104
111
|
const reason = sourceMode === "local"
|
|
105
112
|
? "user_requested_local_snapshot"
|
|
106
113
|
: source.kind === "directory_candidate"
|
|
@@ -136,6 +143,11 @@ function buildFallbackLocalArchivePlan(source, sourceMode) {
|
|
|
136
143
|
}
|
|
137
144
|
function ensurePlannedSources(selection, resolve) {
|
|
138
145
|
if (resolve.plannedSources.length > 0) {
|
|
146
|
+
for (const source of selection.sources) {
|
|
147
|
+
if (!source.absolutePath && !resolve.plannedSources.some((plan) => plan.path === source.path && plan.sourceKind === "remote_repo")) {
|
|
148
|
+
throw new Error("The remote repository is not accessible. Connect its provider or clone it locally, then retry.");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
139
151
|
return resolve;
|
|
140
152
|
}
|
|
141
153
|
if (selection.sourceMode === "remote") {
|
|
@@ -172,6 +184,9 @@ export async function ensureAuthenticated(client, flags) {
|
|
|
172
184
|
if (isClaimProofAvailable(credentials)) {
|
|
173
185
|
throw new Error("Your guest trial session is no longer available, but its claim proof is stored on this machine. Run `apex claim` to keep the trial results.");
|
|
174
186
|
}
|
|
187
|
+
if (!canPromptInteractively(flags)) {
|
|
188
|
+
throw new Error("Apex is not authenticated. Run `apex login` explicitly, then retry this command.");
|
|
189
|
+
}
|
|
175
190
|
return login(client, {
|
|
176
191
|
noOpen: flags["no-open"] === true,
|
|
177
192
|
quiet: isJsonMode(flags),
|
|
@@ -182,16 +197,21 @@ export async function resolveWorkspace(client, cwd, me, flags) {
|
|
|
182
197
|
return resolveWorkspaceSelection(client, selection);
|
|
183
198
|
}
|
|
184
199
|
export async function selectWorkspaceTarget(cwd, me, flags) {
|
|
200
|
+
const sourceMode = getSourceMode(flags);
|
|
185
201
|
const loadedBinding = await loadWorkspaceBinding(cwd);
|
|
186
202
|
const company = await chooseCompany(me, flags, loadedBinding);
|
|
187
|
-
const
|
|
203
|
+
const companyBinding = loadedBinding?.companyId === company.id ? loadedBinding : null;
|
|
204
|
+
const workspaceNameSelection = await chooseWorkspaceName(cwd, flags, companyBinding);
|
|
188
205
|
const workspaceName = workspaceNameSelection.workspaceName;
|
|
189
|
-
const binding = selectBindingForWorkspaceName(flags,
|
|
206
|
+
const binding = selectBindingForWorkspaceName(flags, companyBinding, workspaceName);
|
|
190
207
|
const sourceSelection = getFlagList(flags, "repo");
|
|
191
208
|
const discovered = await discoverSources(cwd, sourceSelection);
|
|
192
209
|
if (discovered.sources.length === 0) {
|
|
193
210
|
throw new Error("No local sources were found.");
|
|
194
211
|
}
|
|
212
|
+
if (sourceMode === "local" && discovered.sources.some((source) => source.absolutePath === null)) {
|
|
213
|
+
throw new Error("Remote repository URLs cannot use --source-mode local. Clone the repository locally first.");
|
|
214
|
+
}
|
|
195
215
|
return {
|
|
196
216
|
company,
|
|
197
217
|
workspaceName,
|
|
@@ -199,7 +219,7 @@ export async function selectWorkspaceTarget(cwd, me, flags) {
|
|
|
199
219
|
binding,
|
|
200
220
|
sources: discovered.sources,
|
|
201
221
|
legacyRepositories: extractLegacyRepositories(discovered.sources),
|
|
202
|
-
sourceMode
|
|
222
|
+
sourceMode,
|
|
203
223
|
};
|
|
204
224
|
}
|
|
205
225
|
export async function resolveWorkspaceSelection(client, selection) {
|
package/dist/shell.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { commandStartThreatModel, hasAuditSetupBeta } from "./threat-model.js";
|
|
2
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
|
+
import { commandCancelScan, commandClaim, commandConnect, commandCredits, commandDoctor, commandExportFindings, commandFindingComment, commandFindingStatus, commandFindingTickets, commandFindingLinkTicket, commandFindingSend, commandFindingFeedback, commandFindingFixReview, commandFindings, commandWorkspaceFindings, commandLogout, commandScan, commandScans, commandStatus, commandTelemetry, commandUpdate, commandWorkspace, commandWorkspaceUse, commandWorkspaces, initializeInteractiveSession, openCurrentApexView, } from "./commands.js";
|
|
4
4
|
import { withFlag } from "./args.js";
|
|
5
5
|
import { printCompanyDetails, printInteractiveSessionSummary, printSourceList, printWorkspaceDetails, } from "./output.js";
|
|
6
6
|
import { readLine } from "./prompt.js";
|
|
@@ -13,7 +13,7 @@ const SHELL_COMPLETIONS = [
|
|
|
13
13
|
{ command: "scans" },
|
|
14
14
|
{
|
|
15
15
|
command: "findings",
|
|
16
|
-
args: ["workspace", "comment", "feedback", "fix-review", "tickets", "link-ticket", "send"],
|
|
16
|
+
args: ["workspace", "status", "comment", "feedback", "fix-review", "tickets", "link-ticket", "send"],
|
|
17
17
|
},
|
|
18
18
|
{ command: "export" },
|
|
19
19
|
{ command: "workspaces" },
|
|
@@ -206,11 +206,8 @@ async function runShellCommand(client, cwd, parsed, shellFlags, session) {
|
|
|
206
206
|
await commandClaim(client, cwd, shellFlags);
|
|
207
207
|
return false;
|
|
208
208
|
case "scan": {
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
process.stderr.write("Usage: /scan [standard|audit|ultra|lite|pr] [pr-number]\n");
|
|
212
|
-
return {};
|
|
213
|
-
}
|
|
209
|
+
const first = parsed.args[0];
|
|
210
|
+
const mode = first && ["standard", "audit", "ultra", "lite", "pr"].includes(first) ? first : null;
|
|
214
211
|
const prNumber = mode === "pr" ? parsed.args[1] : null;
|
|
215
212
|
if (mode === "pr" && !prNumber) {
|
|
216
213
|
process.stderr.write("Usage: /scan pr <pr-number>\n");
|
|
@@ -220,11 +217,15 @@ async function runShellCommand(client, cwd, parsed, shellFlags, session) {
|
|
|
220
217
|
process.stderr.write("Usage: /scan pr <pr-number>\n");
|
|
221
218
|
return {};
|
|
222
219
|
}
|
|
223
|
-
|
|
220
|
+
let commandFlags = mode === "pr" && prNumber
|
|
224
221
|
? withFlag(withFlag(shellFlags, "mode", mode), "pr", [prNumber])
|
|
225
222
|
: mode
|
|
226
223
|
? withFlag(shellFlags, "mode", mode)
|
|
227
224
|
: shellFlags;
|
|
225
|
+
const targets = parsed.args.slice(mode ? 1 : 0);
|
|
226
|
+
if (mode !== "pr" && targets.length > 0) {
|
|
227
|
+
commandFlags = withFlag(commandFlags, "repo", targets);
|
|
228
|
+
}
|
|
228
229
|
if (commandFlags.setup !== undefined) {
|
|
229
230
|
await commandStartThreatModel(client, cwd, commandFlags);
|
|
230
231
|
return {};
|
|
@@ -253,6 +254,12 @@ async function runShellCommand(client, cwd, parsed, shellFlags, session) {
|
|
|
253
254
|
await commandScans(client, cwd, shellFlags);
|
|
254
255
|
return {};
|
|
255
256
|
case "findings": {
|
|
257
|
+
if (parsed.args[0] === "status") {
|
|
258
|
+
if (parsed.args.length < 3)
|
|
259
|
+
throw new Error("Usage: /findings status <finding-ref> <open|fixed|acknowledged|in_review|pending_merge|false_positive|wont_fix|skipped> [note]");
|
|
260
|
+
await commandFindingStatus(client, cwd, shellFlags, parsed.args[1], parsed.args[2], parsed.args.slice(3).join(" ") || undefined);
|
|
261
|
+
return {};
|
|
262
|
+
}
|
|
256
263
|
if (parsed.args[0] === "send") {
|
|
257
264
|
if (parsed.args.length !== 3)
|
|
258
265
|
throw new Error("Usage: /findings send <finding-ref> <linear|jira>");
|
package/dist/telemetry.js
CHANGED
|
@@ -283,7 +283,7 @@ function sanitizeShellArgs(parsed) {
|
|
|
283
283
|
commentLengthBucket: bucketLength(parsed.args.slice(feedbackStatus === "invalid" ? 4 : 3).join(" ")),
|
|
284
284
|
};
|
|
285
285
|
}
|
|
286
|
-
if (["fix-review", "tickets", "link-ticket", "send"].includes(action)) {
|
|
286
|
+
if (["status", "fix-review", "tickets", "link-ticket", "send"].includes(action)) {
|
|
287
287
|
return {
|
|
288
288
|
findingRefProvided: Boolean(parsed.args[1]),
|
|
289
289
|
};
|
|
@@ -331,7 +331,7 @@ function safeCliSubcommand(parsed) {
|
|
|
331
331
|
case "export":
|
|
332
332
|
return safeEnum(subcommand, ["findings"]);
|
|
333
333
|
case "findings":
|
|
334
|
-
return safeEnum(subcommand, ["comment", "feedback", "fix-review", "tickets", "link-ticket", "send"]);
|
|
334
|
+
return safeEnum(subcommand, ["comment", "feedback", "status", "fix-review", "tickets", "link-ticket", "send"]);
|
|
335
335
|
case "setup":
|
|
336
336
|
return safeEnum(subcommand, ["all", "codex", "claude", "copilot"]);
|
|
337
337
|
case "service-key":
|
package/dist/threat-model.js
CHANGED
|
@@ -314,7 +314,18 @@ async function interactiveAnswers(state) {
|
|
|
314
314
|
};
|
|
315
315
|
}
|
|
316
316
|
export async function commandThreatModel(client, cwd, flags, action, supplied, emit = true) {
|
|
317
|
-
const
|
|
317
|
+
const selectedDraft = getFlagString(flags, "draft");
|
|
318
|
+
if (!selectedDraft) {
|
|
319
|
+
const binding = await loadWorkspaceBinding(cwd);
|
|
320
|
+
const setupCommand = binding
|
|
321
|
+
? "apex scan --mode standard --setup guided"
|
|
322
|
+
: "apex workspaces";
|
|
323
|
+
throw new ThreatModelUsageError(`Select --draft <id> from the setup response. To create a draft, run ${setupCommand}${binding ? "" : ", bind the intended workspace with apex workspace use <ref>, then run apex scan --mode standard --setup guided"}. Setup uses the workspace's saved repositories and context, not local file selections.`);
|
|
324
|
+
}
|
|
325
|
+
const parsedDraft = uuid.safeParse(selectedDraft);
|
|
326
|
+
if (!parsedDraft.success)
|
|
327
|
+
throw new ThreatModelUsageError("--draft must be a valid draft ID from the setup response.");
|
|
328
|
+
const draftId = parsedDraft.data;
|
|
318
329
|
requireAuditSetupBeta(await requireAccount(client));
|
|
319
330
|
let state;
|
|
320
331
|
if (action === "status" || action === "next") {
|
|
@@ -368,8 +379,15 @@ export async function commandThreatModel(client, cwd, flags, action, supplied, e
|
|
|
368
379
|
return state;
|
|
369
380
|
}
|
|
370
381
|
export async function commandStartThreatModel(client, cwd, flags, emit = true) {
|
|
371
|
-
const setup =
|
|
372
|
-
|
|
382
|
+
const setup = getFlagString(flags, "setup");
|
|
383
|
+
if (setup !== "guided" && setup !== "auto") {
|
|
384
|
+
throw new ThreatModelUsageError("--setup must be guided or auto.");
|
|
385
|
+
}
|
|
386
|
+
const requestedMode = getFlagString(flags, "mode");
|
|
387
|
+
const mode = requestedMode === "ultra" ? "audit" : requestedMode;
|
|
388
|
+
if (mode !== "standard" && mode !== "audit") {
|
|
389
|
+
throw new ThreatModelUsageError("Audit Setup supports --mode standard or --mode audit. Lite and PR scans do not use --setup; remove it and retry the intended scan mode.");
|
|
390
|
+
}
|
|
373
391
|
if (["repo", "pr", "pr-path", "source-mode", "force", "workspace-name"].some((key) => flags[key] !== undefined))
|
|
374
392
|
throw new ThreatModelUsageError("Audit Setup uses the configured workspace repositories and context; local source and force flags are not supported");
|
|
375
393
|
const me = await requireAccount(client);
|
package/dist/types.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cantinasecurity/apex-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.38",
|
|
4
4
|
"description": "Standalone CLI and MCP server for Apex.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -27,18 +27,19 @@
|
|
|
27
27
|
"node": ">=20"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
31
|
-
"ignore": "^7.0.
|
|
30
|
+
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
31
|
+
"ignore": "^7.0.12",
|
|
32
32
|
"proper-lockfile": "^4.1.2",
|
|
33
|
-
"tar": "^7.
|
|
34
|
-
"zod": "^4.
|
|
33
|
+
"tar": "^7.5.22",
|
|
34
|
+
"zod": "^4.6.5"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@types/node": "^22.
|
|
37
|
+
"@types/node": "^22.20.5",
|
|
38
38
|
"@types/proper-lockfile": "^4.1.4",
|
|
39
|
-
"tsx": "^4.
|
|
39
|
+
"tsx": "^4.23.15",
|
|
40
40
|
"typescript": "^5.9.3",
|
|
41
|
-
"
|
|
41
|
+
"vite": "^6.4.4",
|
|
42
|
+
"vitest": "^4.1.11"
|
|
42
43
|
},
|
|
43
44
|
"scripts": {
|
|
44
45
|
"apex": "tsx src/apex.ts",
|
package/skills/apex-cli/SKILL.md
CHANGED
|
@@ -9,7 +9,7 @@ This skill is bundled with Apex CLI and can be installed into Codex with `apex s
|
|
|
9
9
|
|
|
10
10
|
Prefer the Apex MCP tools over running `apex` in the shell when the server is available. The tools installed by `apex setup` use the local stdio server and device login.
|
|
11
11
|
|
|
12
|
-
For a production MCP client that does not need local filesystem access, use the hosted Streamable HTTP endpoint at `https://ai.cantina.xyz/mcp`. Read the organization service key from `APEX_SERVICE_KEY` and supply it only as `Authorization: Bearer <TOKEN>`; never put the raw key in a tool argument, result, prompt, source file, or log. The hosted tools are `apex-credits`, `apex-workspaces`, `apex-scan`, `apex-black-start`, `apex-status`, `apex-cancel-scan`, `apex-findings`, `apex-workspace-findings`, `apex-finding-tickets`, `apex-finding-link-ticket`, and `apex-finding-
|
|
12
|
+
For a production MCP client that does not need local filesystem access, use the hosted Streamable HTTP endpoint at `https://ai.cantina.xyz/mcp`. Read the organization service key from `APEX_SERVICE_KEY` and supply it only as `Authorization: Bearer <TOKEN>`; never put the raw key in a tool argument, result, prompt, source file, or log. The hosted tools are `apex-credits`, `apex-workspaces`, `apex-scan`, `apex-black-start`, `apex-status`, `apex-cancel-scan`, `apex-findings`, `apex-workspace-findings`, `apex-finding-tickets`, `apex-finding-link-ticket`, `apex-finding-send`, and `apex-finding-status`.
|
|
13
13
|
|
|
14
14
|
OAuth-capable clients may instead use the hosted endpoint’s OAuth discovery and Cantina manager-consent flow. Let the client manage OAuth credentials; never pass them to tools, the REST API, or local CLI. Local stdio MCP continues to use device login.
|
|
15
15
|
|
|
@@ -153,3 +153,19 @@ Keep `policy.version` at `3.1`. Supply a name, instructions, and all eight metri
|
|
|
153
153
|
allowed value and nonempty guidance for each allowed value. Omit disallowed
|
|
154
154
|
values. If Audit Setup or another workspace update is active, wait for it to
|
|
155
155
|
finish before saving again.
|
|
156
|
+
|
|
157
|
+
Scan target and recovery rules:
|
|
158
|
+
- CLI `apex scan [target...]` and repeatable `--repo` accept exact files, directories, or HTTPS GitHub/GitLab repository URLs; local MCP uses `repoPaths` for the same selections. No target means `cwd`.
|
|
159
|
+
- A file selection scans only that file, respecting ignore rules. Local snapshots exclude `.apex/` state. Remote URLs use the default branch and cannot fall back to uploading `cwd`; clone locally to choose a branch or commit.
|
|
160
|
+
- Use Standard for local changes, files, plain directories, and public repositories without provider access. Lite and Audit still require connected provider sources; do not silently substitute another mode.
|
|
161
|
+
- Unauthenticated headless CLI commands fail with explicit login guidance; they never start device login. Use the MCP auth tools for explicit MCP login. Retry transient refresh failures without clearing credentials.
|
|
162
|
+
- Audit Setup is an explicit Standard/Audit beta flow using saved workspace repositories and context. It does not preserve local targets. Never automatically substitute setup after `AUDIT_SETUP_REQUIRED`; use the error's selected-workspace command only when that saved scope is intended. Draft operations require the ID returned by preparation; eligible accounts receive setup commands in CLI help. Lite and PR do not use `--setup`.
|
|
163
|
+
|
|
164
|
+
Finding lifecycle:
|
|
165
|
+
- Close resolved issues with `apex-finding-status` and `status: "fixed"`; reopen with `status: "open"`.
|
|
166
|
+
- Local MCP takes `findingRef`, optional `cwd`/`scanId`, `note`, and `expectedUpdatedAt`, using device login. Hosted MCP takes `findingId` (UUID), no local paths, and requires explicit `findings:write`.
|
|
167
|
+
- The scripted equivalent is `apex findings status <finding-ref> <open|fixed> --note "rationale" --json`. Other statuses: acknowledged, in_review, pending_merge, false_positive, wont_fix, skipped.
|
|
168
|
+
- Status changes preserve scanner validity, severity, feedback, and linked ticket state. Invalid or duplicate findings cannot be reopened into the open queue until their classification is reviewed.
|
|
169
|
+
- Read `remediationStatus` in workspace finding lists for the current lifecycle; `status` remains the scanner verdict. `open` and `acknowledged` stay distinct even though both use the `accepted` review state.
|
|
170
|
+
- Use the finding updatedAt as expectedUpdatedAt when working from a prior snapshot. On FINDING_STATUS_CONFLICT, reload and review before retrying.
|
|
171
|
+
- Linear sync is Linear-to-Apex: started becomes acknowledged, completed becomes fixed. Do not promise Apex-to-Linear status updates or automatic Linear reopening.
|