@cantinasecurity/apex-cli 0.1.37 → 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.
@@ -13,7 +13,7 @@ This skill is bundled with Apex CLI and can be installed into the current reposi
13
13
 
14
14
  Use Apex through the MCP server when the `apex-*` tools are available. The tools installed by `apex setup` use the local stdio server and device login.
15
15
 
16
- 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-send`.
16
+ 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`.
17
17
 
18
18
  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.
19
19
 
@@ -160,3 +160,12 @@ Scan target and recovery rules:
160
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
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
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.
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Cantina agent plugins for security review workflows.",
9
- "version": "0.1.37"
9
+ "version": "0.1.38"
10
10
  },
11
11
  "plugins": [
12
12
  {
@@ -14,10 +14,10 @@
14
14
  "source": {
15
15
  "source": "npm",
16
16
  "package": "@cantinasecurity/apex-cli",
17
- "version": "0.1.37"
17
+ "version": "0.1.38"
18
18
  },
19
19
  "description": "Run Apex security scans and review findings from Claude Code.",
20
- "version": "0.1.37",
20
+ "version": "0.1.38",
21
21
  "author": {
22
22
  "name": "Cantina",
23
23
  "email": "support@cantina.xyz"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apex-cli",
3
- "version": "0.1.37",
3
+ "version": "0.1.38",
4
4
  "description": "Run Apex security scans and review findings from Claude Code.",
5
5
  "author": {
6
6
  "name": "Cantina",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apex-cli",
3
- "version": "0.1.37",
3
+ "version": "0.1.38",
4
4
  "description": "Run Apex security scans and review findings from Codex.",
5
5
  "author": {
6
6
  "name": "Cantina",
package/.mcp.claude.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "args": [
6
6
  "-y",
7
7
  "-p",
8
- "@cantinasecurity/apex-cli@0.1.37",
8
+ "@cantinasecurity/apex-cli@0.1.38",
9
9
  "apex-mcp"
10
10
  ],
11
11
  "env": {
package/.mcp.codex.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "args": [
5
5
  "-y",
6
6
  "-p",
7
- "@cantinasecurity/apex-cli@0.1.37",
7
+ "@cantinasecurity/apex-cli@0.1.38",
8
8
  "apex-mcp"
9
9
  ],
10
10
  "env": {
package/MARKETPLACE.md CHANGED
@@ -13,7 +13,7 @@ This package is prepared as both a Claude Code plugin and a Codex plugin. The pu
13
13
  The plugin MCP configs launch the pinned npm CLI package with:
14
14
 
15
15
  ```bash
16
- npx -y -p @cantinasecurity/apex-cli@0.1.37 apex-mcp
16
+ npx -y -p @cantinasecurity/apex-cli@0.1.38 apex-mcp
17
17
  ```
18
18
 
19
19
  That keeps marketplace installs independent of a user's global `apex` install.
package/README.md CHANGED
@@ -158,30 +158,30 @@ unclaimed local trial proof so `apex claim` can still use it.
158
158
  Agent-safe copy/paste prompt (substitute the issued code):
159
159
 
160
160
  ```text
161
- Install and run Cantina Apex CLI 0.1.37 to start one free standard security scan of this repository immediately.
161
+ Install and run Cantina Apex CLI 0.1.38 to start one free standard security scan of this repository immediately.
162
162
 
163
163
  Use this trial code only where <TRIAL_CODE> appears below: SPBT-DEVREL-XXXXXXXX
164
164
 
165
165
  Run the commands yourself from the repository root; do not stop after setup or only explain the steps. Use terminal CLI commands only. Do not use an Apex MCP tool or run `apex setup`, `apex mcp`, `apex login`, `apex-auth-start`, or `apex-auth-wait`. Never use `--replace` or `--force`.
166
166
 
167
167
  Use this exact command prefix for every Apex command:
168
- npx -y -p @cantinasecurity/apex-cli@0.1.37 apex
168
+ npx -y -p @cantinasecurity/apex-cli@0.1.38 apex
169
169
 
170
170
  1. Before redeeming, confirm every command will use the same persistent, user-accessible machine or container, HOME, filesystem, and repository root. Confirm Node.js 20+ and npx are available. If `APEX_BASE_URL` is set, continue only when its origin is `https://ai.cantina.xyz`. Run the exact prefix with `--help` and continue only if help lists both `redeem` and `claim`. If any check fails, STOP before sending the code; do not install system packages.
171
171
  2. Redeem, with at most one response-loss recovery retry:
172
- npx -y -p @cantinasecurity/apex-cli@0.1.37 apex redeem <TRIAL_CODE> --json
172
+ npx -y -p @cantinasecurity/apex-cli@0.1.38 apex redeem <TRIAL_CODE> --json
173
173
  This terminal command is the only allowed tool call containing the code. Never echo or restate the code, store it in a file or environment variable, put it in a URL or MCP call, or include it in your response. Parse the JSON from stdout only; do not merge stderr into stdout, because npm may write warnings to stderr. Continue when the command exits 0 and stdout is JSON with `redeemed: true` and `outcome` equal to `redeemed` or `idempotent`. `idempotent` means the existing guest session resumed and it may already own a workspace. If and only if the first attempt ends with an ambiguous transport/connection failure or an interrupted/missing response and no typed server error code, retry this exact same command once on the same machine, HOME, and repository root. Never retry a typed server rejection and never make more than one recovery retry. If the allowed retry does not return a successful JSON outcome, STOP.
174
174
  3. Immediately start exactly one scan; do not wait for another instruction:
175
- npx -y -p @cantinasecurity/apex-cli@0.1.37 apex scan --mode standard --non-interactive --no-open --json
175
+ npx -y -p @cantinasecurity/apex-cli@0.1.38 apex scan --mode standard --non-interactive --no-open --json
176
176
  The CLI validates the complete guest local archive before it reserves a workspace. An unbound renamed checkout may recover only the sole empty guest workspace when no explicit workspace name was supplied. On any error or nonzero exit, STOP. After a successful scan JSON response, inspect `localPersistence.workspaceBindingSaved`. If it is `false`, the server scan already started: capture and report `binding.lastScanId`, then STOP; do not run status or findings until local repository permissions are repaired. Otherwise capture `binding.lastScanId` from the JSON as <SCAN_ID>; if it is absent, STOP.
177
177
  4. About every 60 seconds, poll that exact scan:
178
- npx -y -p @cantinasecurity/apex-cli@0.1.37 apex status --scan <SCAN_ID> --json
178
+ npx -y -p @cantinasecurity/apex-cli@0.1.38 apex status --scan <SCAN_ID> --json
179
179
  Read `progress.progress.status`. Wait while it is `created`, `pending`, `queued`, `starting`, `running`, `in_progress`, or `processing`. Continue on `completed`. STOP on `failed`, `cancelled`, a missing or unknown status, or a command failure.
180
180
  5. Fetch that exact scan's findings:
181
- npx -y -p @cantinasecurity/apex-cli@0.1.37 apex findings --scan <SCAN_ID> --json
181
+ npx -y -p @cantinasecurity/apex-cli@0.1.38 apex findings --scan <SCAN_ID> --json
182
182
  On success, summarize findings by severity, most severe first, including identifier, title, status, and confidence. This command does not return file locations, so do not invent them. On failure, STOP.
183
183
  6. Do not claim automatically. Tell me the claim deadline from step 2 and remind me to run this exact command myself from the same repository root, machine, and HOME:
184
- npx -y -p @cantinasecurity/apex-cli@0.1.37 apex claim
184
+ npx -y -p @cantinasecurity/apex-cli@0.1.38 apex claim
185
185
  It may open a browser so I can create or sign in to a Cantina account and keep the results. Claim moves the trial into the currently selected Cantina workspace when I can run scans there, or into my existing personal workspace. If the selected workspace cannot run scans, tell me to switch to one that can and retry before the claim deadline.
186
186
 
187
187
  Never paste raw command transcripts. For any failure, report only the step, exit code, and sanitized error. Redact the trial code and every `apex_at_`, `apex_rt_`, or `apex_ct_` value from all reports.
@@ -222,6 +222,7 @@ Supported shell commands:
222
222
  - `/findings send <finding-ref> <linear|jira>`
223
223
  - `/findings link-ticket <finding-ref> <linear|jira> <issue-key|url>`
224
224
  - `/findings comment <finding-id|finding-identifier> <comment>`
225
+ - `/findings status <finding-ref> <open|fixed|acknowledged|in_review|pending_merge|false_positive|wont_fix|skipped> [note]`
225
226
  - `/findings feedback <finding-id|finding-identifier> valid [comment]`
226
227
  - `/findings feedback <finding-id|finding-identifier> invalid <false-positive|by-design|not-relevant> [comment]`
227
228
  - `/findings fix-review <finding-id|finding-identifier>`
@@ -260,6 +261,7 @@ Supported shell commands:
260
261
  - `apex findings send <finding-ref> <linear|jira> [--scan <scan-id>] [--json]`
261
262
  - `apex findings link-ticket <finding-ref> <linear|jira> <issue-key|url> [--scan <scan-id>] [--json]`
262
263
  - `apex findings comment <finding-id|finding-identifier> --content <markdown> [--parent-comment <comment-id>] [--scan <scan-id>]`
264
+ - `apex findings status <finding-ref> <open|fixed|acknowledged|in_review|pending_merge|false_positive|wont_fix|skipped> [--note <rationale>] [--expected-updated-at <timestamp>] [--scan <scan-id>]`
263
265
  - `apex findings feedback <finding-id|finding-identifier> <valid|invalid> [comment] [--comment <markdown>] [--scan <scan-id>] [--suggested-severity extreme|critical|high|medium|low|informational] [--dismissal-reason false-positive|by-design|not-relevant] [--label acknowledged|fixed] [--fix-pr-url <github-pr-url>]`
264
266
  - `apex findings fix-review <finding-id|finding-identifier> [--scan <scan-id>] [--review-target pull-request|repository-current] [--fix-pr-url <github-pr-url>]`
265
267
  - `apex export findings [--scan <scan-id>] [--format markdown|json|gitlab-sast] [--output <path>]`
@@ -310,7 +312,7 @@ The CLI automatically uses your company when only one is available. If you belon
310
312
 
311
313
  The secret is returned only by `create` and cannot be retrieved later. Store it directly in a secret manager and do not put it in source control, prompts, chat messages, or logs. `--json` also includes the shown-once secret so it can be captured programmatically; handle that output as a credential.
312
314
 
313
- Service keys are organization-level credentials. Grant only the scopes the integration needs: `scans:create` reads credit availability and triggers scans, `scans:read` polls status, and `findings:read` retrieves findings. Omit `--scope` to grant those three default scopes. Ticket linking additionally requires explicitly requesting `findings:write`. For a P1-only adapter that reads manually started scans, create a read-only key with `--scope scans:read,findings:read`.
315
+ Service keys are organization-level credentials. Grant only the scopes the integration needs: `scans:create` reads credit availability and triggers scans, `scans:read` polls status, and `findings:read` retrieves findings. Omit `--scope` to grant those three default scopes. Finding status changes and ticket management additionally require explicitly requesting `findings:write`. For a P1-only adapter that reads manually started scans, create a read-only key with `--scope scans:read,findings:read`.
314
316
 
315
317
  A key with `findings:read` can retrieve unpublished draft findings for scans in its organization so an external hub can perform triage. Give the key only to systems that are allowed to handle those drafts.
316
318
 
@@ -356,7 +358,7 @@ Claude Code can expand the same environment variable from `.mcp.json`:
356
358
  }
357
359
  ```
358
360
 
359
- The hosted server exposes `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-send`. Ticket creation and linking require an explicit `findings:write` scope and a connected Linear/Jira integration. Hosted operations are scoped to the authenticated organization. Repository scans use connected GitHub repositories; Apex Black scans use explicitly authorized HTTPS origins. Use local stdio MCP for device auth, local filesystem/archive access, provider connection, setup, comments, feedback, fix reviews, and trial flows.
361
+ The hosted server exposes `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`. Ticket creation and linking require an explicit `findings:write` scope and a connected Linear/Jira integration. Hosted operations are scoped to the authenticated organization. Repository scans use connected GitHub repositories; Apex Black scans use explicitly authorized HTTPS origins. Use local stdio MCP for device auth, local filesystem/archive access, provider connection, setup, comments, feedback, fix reviews, and trial flows.
360
362
 
361
363
  Apex Black is hosted-only: use `apex-black-start` when the connected deployment advertises it and the organization has Apex Black beta access. Check `apex-credits` first. Supply 1–16 exact HTTPS origins, a stable `idempotencyKey`, and `authorizationAccepted: true` only when the user has authorized every target and accepted the 200 Standard credit cost; honor authorization already provided in the session. Use the returned Bedrock `scanId` with `apex-status` and `apex-findings`. An identical retry replays a stored start failure, including `APEX_BLACK_START_FAILED`; retain its durable IDs and contact support before trying another request. Local CLI and stdio MCP do not expose this web-scan tool or a Black scan mode.
362
364
 
@@ -617,6 +619,7 @@ The local stdio MCP server exposes Apex-specific tools for:
617
619
  - cross-scan workspace findings through `apex-workspace-findings`, including direct workspace selection, first-class open state, web UI lifecycle fields, and server-side filters
618
620
  - Lite scan start by calling `apex-scan` with `mode: "lite"`
619
621
  - PR scan start by calling `apex-scan` with `mode: "pr"` and `pullRequests`
622
+ - close/reopen findings with `apex-finding-status`, preserving scanner validity and feedback
620
623
  - finding comments and valid/invalid feedback with `apex-finding-comment` and `apex-finding-feedback`
621
624
  - Create Linear/Jira tickets with `apex-finding-send` using the saved frontend destination
622
625
  - Link existing Linear/Jira tickets with `apex-finding-link-ticket` and inspect links with `apex-finding-tickets`
@@ -684,7 +687,7 @@ Telemetry event posts do not include bearer tokens. The telemetry endpoint is `P
684
687
  {
685
688
  "schemaVersion": 1,
686
689
  "event": "apex.invocation.completed",
687
- "cliVersion": "0.1.37",
690
+ "cliVersion": "0.1.38",
688
691
  "invocation": {
689
692
  "surface": "mcp",
690
693
  "command": "apex-scan",
@@ -715,7 +718,7 @@ The npm package also includes marketplace-ready plugin artifacts:
715
718
  - `.claude-plugin/plugin.json` and `.mcp.claude.json` for Claude Code plugin installs
716
719
  - `.claude-plugin/marketplace.json` for a Claude marketplace entry backed by the public npm package
717
720
 
718
- These plugin installs launch the pinned npm package with `npx -y -p @cantinasecurity/apex-cli@0.1.37 apex-mcp`, so users do not need to install `apex` globally before enabling the plugin.
721
+ These plugin installs launch the pinned npm package with `npx -y -p @cantinasecurity/apex-cli@0.1.38 apex-mcp`, so users do not need to install `apex` globally before enabling the plugin.
719
722
 
720
723
  The repository also includes `.agents/plugins/marketplace.json` for local Codex marketplace testing from a checkout.
721
724
 
@@ -785,3 +788,35 @@ Keep `policy.version` at `3.1`. Supply a name, instructions, and all eight metri
785
788
  allowed value and nonempty guidance for each allowed value. Omit disallowed
786
789
  values. If Audit Setup or another workspace update is active, wait for it to
787
790
  finish before saving again.
791
+
792
+ ### Close and reopen findings
793
+
794
+ Use `apex-finding-status` to manage remediation directly from either MCP transport.
795
+ Local MCP accepts `findingRef` and optional `cwd`/`scanId` using device login.
796
+ Hosted MCP accepts `findingId` (UUID) and requires explicitly granted `findings:write`.
797
+ Pass `status: "fixed"` to close an issue or `status: "open"` to reopen it.
798
+
799
+ ```sh
800
+ apex findings status KERN2-25 fixed --note "Resolved in PR #123" --json
801
+ apex findings status KERN2-25 open --note "Regression reproduced" --json
802
+ ```
803
+
804
+ Other statuses are `acknowledged`, `in_review`, `pending_merge`, `false_positive`,
805
+ `wont_fix`, and `skipped`. Reopening stores `accepted`, retaining the fact that the
806
+ finding was reviewed. Status changes preserve scanner validity, severity, and
807
+ valid/invalid feedback; invalid or duplicate findings must have their classification
808
+ reviewed before they can enter the open queue.
809
+
810
+ REST integrations call `PATCH /api/apex/v1/findings/{findingId}/status` with
811
+ `{"status":"fixed","note":"Resolved in PR #123"}` and a bearer service key with
812
+ `findings:write`. Each change is audited. The response includes `findingId`,
813
+ `status`, `reviewState`, `fromReviewState`, `updatedAt`, and `changed`.
814
+ Workspace finding lists expose `remediationStatus`, keeping `open` and `acknowledged`
815
+ distinct from the scanner `status` and the persisted `reviewState`.
816
+ Pass the current finding `updatedAt` as `expectedUpdatedAt` (CLI:
817
+ `--expected-updated-at`) to reject stale edits with `FINDING_STATUS_CONFLICT` (409).
818
+ Reload and review the latest state before retrying a conflict.
819
+
820
+ Linear sync currently runs from Linear to Apex: started tickets become acknowledged
821
+ and completed tickets become fixed. Apex status changes do not update linked Linear
822
+ or Jira tickets. Use the direct status operation to manage issues from MCP or REST.
package/dist/apex.js CHANGED
@@ -3,7 +3,7 @@ import { commandStartThreatModel, commandThreatModel, hasAuditSetupBeta, threatM
3
3
  import { getMe } from "./auth.js";
4
4
  import { ApexApiClient, formatApiError } from "./api-client.js";
5
5
  import { isAuditSetupInvocation, parseArgs, validateCommandFlags } from "./args.js";
6
- import { commandCancelScan, commandClaim, commandConnect, commandCredits, commandDoctor, commandExportFindings, commandFindingComment, commandFindingTickets, commandFindingLinkTicket, commandFindingSend, commandFindingFeedback, commandFindingFixReview, commandFindings, commandWorkspaceFindings, commandLogin, commandLogout, commandRedeem, commandScan, commandScans, commandServiceKeyCreate, commandServiceKeyList, commandServiceKeyRevoke, commandSetup, commandStatus, commandTelemetry, commandUpdate, commandWorkspace, commandWorkspaceUse, commandWorkspaces, getRedeemExitCode, } from "./commands.js";
6
+ import { commandCancelScan, commandClaim, commandConnect, commandCredits, commandDoctor, commandExportFindings, commandFindingComment, commandFindingStatus, commandFindingTickets, commandFindingLinkTicket, commandFindingSend, commandFindingFeedback, commandFindingFixReview, commandFindings, commandWorkspaceFindings, commandLogin, commandLogout, commandRedeem, commandScan, commandScans, commandServiceKeyCreate, commandServiceKeyList, commandServiceKeyRevoke, commandSetup, commandStatus, commandTelemetry, commandUpdate, commandWorkspace, commandWorkspaceUse, commandWorkspaces, getRedeemExitCode, } from "./commands.js";
7
7
  import { cliHelpText } from "./help.js";
8
8
  import { runMcpServer } from "./mcp.js";
9
9
  import { runInteractiveShell } from "./shell.js";
@@ -109,6 +109,12 @@ async function dispatch(parsed) {
109
109
  await commandWorkspace(cwd, parsed.flags);
110
110
  return;
111
111
  case "findings":
112
+ if (parsed.subcommand === "status") {
113
+ if (parsed.args.length !== 2)
114
+ throw new Error("Usage: apex findings status <finding-ref> <open|fixed|acknowledged|in_review|pending_merge|false_positive|wont_fix|skipped> [--note <rationale>]");
115
+ await commandFindingStatus(client, cwd, parsed.flags, parsed.args[0], parsed.args[1], typeof parsed.flags.note === "string" ? parsed.flags.note : undefined, typeof parsed.flags["expected-updated-at"] === "string" ? parsed.flags["expected-updated-at"] : undefined);
116
+ return;
117
+ }
112
118
  if (parsed.subcommand === "send") {
113
119
  if (parsed.args.length !== 2)
114
120
  throw new Error("Usage: apex findings send <finding-ref> <linear|jira>");
package/dist/args.js CHANGED
@@ -14,6 +14,7 @@ export const KNOWN_CLI_FLAG_NAMES = [
14
14
  "content",
15
15
  "dismissal-reason",
16
16
  "expires-at",
17
+ "expected-updated-at",
17
18
  "fix-pr-url",
18
19
  "force",
19
20
  "format",
@@ -25,6 +26,7 @@ export const KNOWN_CLI_FLAG_NAMES = [
25
26
  "limit",
26
27
  "mode",
27
28
  "name",
29
+ "note",
28
30
  "no-open",
29
31
  "non-interactive",
30
32
  "output",
package/dist/commands.js CHANGED
@@ -4,7 +4,8 @@ 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, fetchFindingTickets, linkFindingTicket, sendFindingToIssueTracker, 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, setFindingStatus, } from "./findings.js";
8
+ import { FINDING_LIFECYCLE_STATUSES } from "./types.js";
8
9
  import { prepareLocalSourceArchives, prepareExplicitScanSources, supportsLegacyRemoteFlow, } from "./local-source-scan.js";
9
10
  import { logLine, printJson, withLoadingIndicator } from "./output.js";
10
11
  import { claimOnboardingSession, isProofOnlyOnboardingCredentials, redeemOnboardingCode, revokeGuestOnboardingSession, } from "./onboarding.js";
@@ -1896,7 +1897,7 @@ export async function openCurrentApexView(client, cwd, flags) {
1896
1897
  }
1897
1898
  return url;
1898
1899
  }
1899
- async function resolveTicketFinding(client, cwd, flags, requestedFindingRef) {
1900
+ async function resolveCommandFinding(client, cwd, flags, requestedFindingRef) {
1900
1901
  const findingRef = normalizeFindingRefInput(requestedFindingRef);
1901
1902
  const currentBinding = await loadWorkspaceBinding(cwd);
1902
1903
  const binding = isFindingUuid(findingRef)
@@ -1907,7 +1908,7 @@ async function resolveTicketFinding(client, cwd, flags, requestedFindingRef) {
1907
1908
  requestedScanId: getFlagString(flags, "scan") });
1908
1909
  }
1909
1910
  export async function commandFindingTickets(client, cwd, flags, findingRef) {
1910
- const selection = await resolveTicketFinding(client, cwd, flags, findingRef);
1911
+ const selection = await resolveCommandFinding(client, cwd, flags, findingRef);
1911
1912
  const result = await fetchFindingTickets(client, selection.findingId);
1912
1913
  const payload = { findingId: selection.findingId, findingRef: selection.findingRef, ...result };
1913
1914
  if (isJsonMode(flags))
@@ -1923,7 +1924,7 @@ export async function commandFindingLinkTicket(client, cwd, flags, findingRef, p
1923
1924
  if ((provider !== "linear" && provider !== "jira") || !issueRef.trim() || issueRef.trim().length > 2048) {
1924
1925
  throw new Error("Usage: apex findings link-ticket <finding-ref> <linear|jira> <issue-key|url>");
1925
1926
  }
1926
- const selection = await resolveTicketFinding(client, cwd, flags, findingRef);
1927
+ const selection = await resolveCommandFinding(client, cwd, flags, findingRef);
1927
1928
  const result = await linkFindingTicket(client, selection.findingId, { provider, issueRef: issueRef.trim() });
1928
1929
  const payload = { findingId: selection.findingId, findingRef: selection.findingRef, ...result };
1929
1930
  if (isJsonMode(flags))
@@ -1936,7 +1937,7 @@ export async function commandFindingSend(client, cwd, flags, findingRef, provide
1936
1937
  if (provider !== "linear" && provider !== "jira") {
1937
1938
  throw new Error("Usage: apex findings send <finding-ref> <linear|jira>");
1938
1939
  }
1939
- const selection = await resolveTicketFinding(client, cwd, flags, findingRef);
1940
+ const selection = await resolveCommandFinding(client, cwd, flags, findingRef);
1940
1941
  const result = await sendFindingToIssueTracker(client, selection.findingId, provider);
1941
1942
  const payload = { findingId: selection.findingId, findingRef: selection.findingRef, ...result };
1942
1943
  if (isJsonMode(flags))
@@ -1945,3 +1946,29 @@ export async function commandFindingSend(client, cwd, flags, findingRef, provide
1945
1946
  logLine(`${result.created ? "Created" : "Already sent"}: ${result.export.issueKey ?? result.export.issueId} ${result.export.issueUrl}`, flags);
1946
1947
  return payload;
1947
1948
  }
1949
+ export async function commandFindingStatus(client, cwd, flags, findingRef, status, note, expectedUpdatedAt) {
1950
+ if (!FINDING_LIFECYCLE_STATUSES.includes(status)) {
1951
+ throw new Error(`Usage: apex findings status <finding-ref> <${FINDING_LIFECYCLE_STATUSES.join("|")}> [--note <rationale>]`);
1952
+ }
1953
+ const trimmedNote = note?.trim();
1954
+ if (trimmedNote && trimmedNote.length > 1000)
1955
+ throw new Error("Status notes must be 1000 characters or less.");
1956
+ if (expectedUpdatedAt && (!/^\d{4}-\d{2}-\d{2}T/.test(expectedUpdatedAt) || !Number.isFinite(Date.parse(expectedUpdatedAt)))) {
1957
+ throw new Error("--expected-updated-at must be an ISO timestamp from the finding's updatedAt.");
1958
+ }
1959
+ const selection = await resolveCommandFinding(client, cwd, flags, findingRef);
1960
+ const result = await setFindingStatus(client, selection.findingId, {
1961
+ status: status,
1962
+ ...(trimmedNote ? { note: trimmedNote } : {}),
1963
+ ...(expectedUpdatedAt ? { expectedUpdatedAt } : {}),
1964
+ });
1965
+ const payload = { ...result, findingRef: selection.findingRef };
1966
+ if (isJsonMode(flags))
1967
+ printJson(payload);
1968
+ else {
1969
+ logLine(`Finding: ${selection.findingRef}`, flags);
1970
+ logLine(`Status: ${result.status} (${result.reviewState})${result.changed ? "" : " — already set"}`, flags);
1971
+ logLine("Scanner validity and review feedback are unchanged.", flags);
1972
+ }
1973
+ return payload;
1974
+ }
package/dist/findings.js CHANGED
@@ -5,7 +5,7 @@ const WORKSPACE_FINDING_FILTER_PATTERN = /^!?(severity|scan|repo|review|impact|l
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, 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.";
8
+ const FINDING_WRITE_AUTH_ERROR = "Finding comments, feedback, status changes, 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;
@@ -321,6 +321,11 @@ export async function startFindingFixReviewScan(client, findingId, input = {}) {
321
321
  export async function fetchFindingTickets(client, findingId) {
322
322
  return requestFindingAppRoute(client, `/api/findings/${encodeURIComponent(findingId)}/tickets`);
323
323
  }
324
+ export async function setFindingStatus(client, findingId, input) {
325
+ return requestFindingAppRoute(client, `/api/findings/${encodeURIComponent(findingId)}/status`, {
326
+ method: "PATCH", json: input,
327
+ });
328
+ }
324
329
  export async function linkFindingTicket(client, findingId, input) {
325
330
  return requestFindingAppRoute(client, `/api/findings/${encodeURIComponent(findingId)}/tickets`, {
326
331
  method: "POST", json: input,
package/dist/help.js CHANGED
@@ -14,6 +14,8 @@ export const CLI_HELP_TEXT = `Usage:
14
14
  Add a comment or note to a finding
15
15
  apex findings feedback <finding-id|finding-identifier> <valid|invalid>
16
16
  Leave feedback; invalid requires --dismissal-reason
17
+ apex findings status <finding-ref> <open|fixed|acknowledged|in_review|pending_merge|false_positive|wont_fix|skipped>
18
+ Close, reopen, or update client remediation status
17
19
  apex findings fix-review <finding-id|finding-identifier>
18
20
  Start a fix review scan for a Fix PR or current repository
19
21
  apex export findings Export findings for the latest or selected scan
@@ -56,6 +58,8 @@ Flags:
56
58
  --status valid|invalid Set the finding feedback status explicitly
57
59
  --content <markdown> Supply comment content for apex findings comment
58
60
  --comment <markdown> Supply rationale for apex findings feedback
61
+ --note <rationale> Audit note for a finding status change (max 1000 characters)
62
+ --expected-updated-at <timestamp> Reject a status change if the finding has changed since this updatedAt
59
63
  --parent-comment <comment-id> Reply to an existing finding comment
60
64
  --suggested-severity extreme|critical|high|medium|low|informational
61
65
  Attach a suggested severity to valid feedback
@@ -163,6 +167,8 @@ Commands:
163
167
  Add a comment or note to a finding
164
168
  /findings feedback <finding-ref> valid [comment]
165
169
  Leave valid feedback on a finding
170
+ /findings status <finding-ref> <open|fixed|acknowledged|in_review|pending_merge|false_positive|wont_fix|skipped> [note]
171
+ Close, reopen, or update client remediation status
166
172
  /findings feedback <finding-ref> invalid <reason> [comment]
167
173
  Leave invalid feedback; reason is false-positive, by-design, or not-relevant
168
174
  /findings fix-review <finding-ref>
package/dist/mcp.js CHANGED
@@ -6,11 +6,12 @@ import { z } from "zod";
6
6
  import { getMe, logout, readStableDeviceLoginState, startDeviceLogin, waitForDeviceLoginApproval, } from "./auth.js";
7
7
  import { ApexApiClient, formatApiError } from "./api-client.js";
8
8
  import { getClaimProofState, isClaimProofAvailable, } from "./claim-proof.js";
9
- import { commandCancelScan, commandConnect, commandCredits, commandDoctor, commandExportFindings, commandFindingComment, commandFindingTickets, commandFindingLinkTicket, commandFindingSend, commandFindingFeedback, commandFindingFixReview, commandFindings, commandWorkspaceFindings, commandScan, commandScans, commandStatus, commandWorkspace, commandWorkspaceUse, commandWorkspaces, } from "./commands.js";
9
+ import { commandCancelScan, commandConnect, commandCredits, commandDoctor, commandExportFindings, commandFindingComment, commandFindingStatus, commandFindingTickets, commandFindingLinkTicket, commandFindingSend, commandFindingFeedback, commandFindingFixReview, commandFindings, commandWorkspaceFindings, commandScan, commandScans, commandStatus, commandWorkspace, commandWorkspaceUse, commandWorkspaces, } from "./commands.js";
10
10
  import { cliHelpText } from "./help.js";
11
11
  import { getLocalTrialStatus, isProofOnlyOnboardingCredentials, revokeGuestOnboardingSession, } from "./onboarding.js";
12
12
  import { createMcpServerTelemetryInvocation, createMcpTelemetryInvocation, emitInvocationCompleted, emitInvocationStarted, emitMcpServerStarted, withTelemetryContext, } from "./telemetry.js";
13
13
  import { APEX_CLI_VERSION } from "./version.js";
14
+ import { FINDING_LIFECYCLE_STATUSES } from "./types.js";
14
15
  const MCP_WORKFLOW_GUIDE = `Use Apex through these tools instead of shelling out to the CLI.
15
16
 
16
17
  Suggested workflow:
@@ -23,7 +24,7 @@ Suggested workflow:
23
24
  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.
24
25
  8. For PR scans, call apex-scan with mode "pr" and one or more pullRequests.
25
26
  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
+ 10. Close or reopen findings with apex-finding-status using status fixed or open. This changes remediation, not scanner validity or feedback. Leave comments or valid/invalid feedback with apex-finding-comment and apex-finding-feedback when review collaboration is needed.
27
28
  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.
28
29
 
29
30
  Notes:
@@ -755,6 +756,22 @@ function registerTools(server, auditSetupEnabled = false) {
755
756
  parentCommentId,
756
757
  scanId,
757
758
  }));
759
+ server.registerTool("apex-finding-status", {
760
+ title: "Change Apex Finding Status",
761
+ description: "Close a finding with fixed or reopen it with open. Changes client remediation only, preserving scanner validity, severity, feedback, and linked ticket status. Invalid or duplicate findings cannot be reopened until their classification is reviewed. Uses device login. Hosted MCP exposes the same tool with findingId and explicit findings:write. Optional expectedUpdatedAt prevents overwriting a newer edit.",
762
+ inputSchema: {
763
+ cwd: z.string().optional(),
764
+ findingRef: z.string(),
765
+ status: z.enum(FINDING_LIFECYCLE_STATUSES),
766
+ note: z.string().trim().max(1000).optional(),
767
+ expectedUpdatedAt: z.iso.datetime({ offset: true }).optional(),
768
+ scanId: z.string().optional(),
769
+ },
770
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
771
+ }, async ({ cwd, findingRef, status, note, expectedUpdatedAt, scanId }) => runTool("apex-finding-status", async () => {
772
+ const targetCwd = resolveCwd(cwd);
773
+ return { cwd: targetCwd, ...await commandFindingStatus(new ApexApiClient(), targetCwd, buildFlags({ scanId }), findingRef, status, note, expectedUpdatedAt) };
774
+ }, (value) => `Set finding ${String(value.findingRef)} to ${String(value.status)}.`, { cwd, findingRef, status, note, expectedUpdatedAt, scanId }));
758
775
  server.registerTool("apex-finding-feedback", {
759
776
  title: "Leave Apex Finding Feedback",
760
777
  description: "Leave valid or invalid feedback on an Apex finding using the current Apex login. To attach a fix PR, send status valid with label fixed and fixPrUrls.",
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" },
@@ -254,6 +254,12 @@ async function runShellCommand(client, cwd, parsed, shellFlags, session) {
254
254
  await commandScans(client, cwd, shellFlags);
255
255
  return {};
256
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
+ }
257
263
  if (parsed.args[0] === "send") {
258
264
  if (parsed.args.length !== 3)
259
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/types.js CHANGED
@@ -1 +1,4 @@
1
- export {};
1
+ export const FINDING_LIFECYCLE_STATUSES = [
2
+ "open", "fixed", "acknowledged", "in_review", "pending_merge",
3
+ "false_positive", "wont_fix", "skipped",
4
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cantinasecurity/apex-cli",
3
- "version": "0.1.37",
3
+ "version": "0.1.38",
4
4
  "description": "Standalone CLI and MCP server for Apex.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -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-send`.
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
 
@@ -160,3 +160,12 @@ Scan target and recovery rules:
160
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
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
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.