@cantinasecurity/apex-cli 0.1.28 → 0.1.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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-status`, `apex-cancel-scan`, `apex-findings`, and `apex-workspace-findings`.
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-status`, `apex-cancel-scan`, `apex-findings`, `apex-workspace-findings`, `apex-finding-tickets`, `apex-finding-link-ticket`, and `apex-finding-send`.
17
17
 
18
18
  For serverless, cron, or other explicit HTTP/JSON integrations, use the hosted Apex REST API instead of CLI or local stdio MCP. The hosted MCP and REST contract is documented at https://ai.cantina.xyz/docs/apex-api.
19
19
 
@@ -54,6 +54,7 @@ If the Apex MCP server is not configured, fall back to the local CLI:
54
54
  - A trial redeem with an ambiguous transport or response-loss failure may be retried exactly once with the same code, machine, HOME, and repository root. Never retry a typed server rejection or use `--replace` for response-loss recovery.
55
55
  - 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.
56
56
  - 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.
57
+ - Shared Standard-scan codes may be custom names such as `TEST100` (1-64 letters, numbers, underscores, or hyphens). Use CLI 0.1.34 or newer. One redemption provides either a guest Standard scan or 100% off one 200-credit checkout package, sharing the same cap. Never redeem both paths with a one-use code. Legacy `APEX-...` and `SPBT-...` codes remain accepted. Do not repeat the real code in output.
57
58
  - `apex-trial-status` is MCP-only; do not invoke it as a CLI command. In a CLI-only flow, use the `claimDeadline` returned by `apex redeem <code> --json` without repeating or persisting the secret code.
58
59
  - After a guest scan, tell the user to run `apex claim` on the same machine before that returned claim deadline. If the deadline is unavailable or uncertain, do not guess or start a replacement login; run `apex claim` and let the server validate the proof.
59
60
  - Claim moves the guest workspace, scans, and findings into the currently selected Cantina workspace when the account can run scans there. Personal workspace owners receive the trial in their existing workspace instead of a duplicate. If the selected workspace cannot run scans, tell the user to switch to one that can and retry before the claim deadline.
@@ -78,6 +79,7 @@ If the Apex MCP server is not configured, fall back to the local CLI:
78
79
  - `apex-workspace-use` accepts a workspace name, prefix, or ID.
79
80
  - Use `sourceMode: "remote"` only when the user explicitly wants to forbid local snapshot fallbacks.
80
81
  - When checking a scan that is not the workspace binding's latest scan, pass `scanId` to `apex-status`; use `apex scans` or `apex-scans` first if you need to discover scan IDs.
82
+ - Use only the Bedrock `scanId` returned by Apex. Kernel execution IDs are private and are not returned or accepted as aliases by CLI or MCP tools.
81
83
  - Use `apex-workspace-findings` when the user asks for findings across a workspace instead of one scan. Set `findingState: "open"` for findings needing review; open excludes fixed, invalid, dismissed, and duplicate findings and is not `status:open`. Pass `company` plus a workspace name, prefix, or ID as `workspaceRef` to avoid relying on or mutating a local binding. In CLI-only flows, use `apex findings --workspace --company <handle> --workspace-ref <ref> --finding-state open`.
82
84
  - Workspace finding responses expose the web UI lifecycle fields `status`, `validity`, `reviewState`, `isDuplicate`, `duplicateOfId`, `repository`, and `sourceFile`. Supported filter facets are `severity`, `scan`, `repo`, `review`, `impact`, `likelihood`, `status`, `validation`, `visibility`, `duplicate`, and `source`; a leading `!` negates a filter.
83
85
  - Finding comments, feedback, and fix review scan starts use the same Apex device-login credentials as read tools. If a write tool reports missing auth, re-run `apex-auth-status`, check `apex-trial-status`, and complete `apex-auth-start` / `apex-auth-wait` only when no unexpired local claim proof is available. Never ask for browser cookies or `CANTINA_AUTH_TOKEN`; an error requiring that variable proves the MCP server is outdated, so update Apex CLI, re-run `apex setup <client>`, restart the client, and retry.
@@ -99,3 +101,18 @@ If the Apex MCP server is not configured, fall back to the local CLI:
99
101
  - Review all open workspace findings: call `apex-workspaces` with the company, then call `apex-workspace-findings` with the selected `workspaceRef` and `findingState: "open"`.
100
102
  - Leave review feedback: call `apex-finding-comment` to add a note, or `apex-finding-feedback` with `status` set to `valid` or `invalid`.
101
103
  - Run a Fix PR review: call `apex-finding-fix-review` with `fixPrUrls`, optionally after recording fixed feedback with `apex-finding-feedback`.
104
+
105
+ ## Send findings to Linear and Jira
106
+
107
+ - Use `apex-finding-send` to create a ticket from a finding, using the connected integration and saved frontend routing. Local input: `findingRef`, `provider: "linear" | "jira"`, optional `cwd`/`scanId`. Hosted input: `findingId` (UUID) and `provider`, with explicit `findings:write` scope.
108
+ - CLI fallback: `apex findings send <finding-ref> <linear|jira> --json`. This uses device login and stays non-interactive in automation.
109
+ - Connect the integration and configure its destination in Apex first: Linear workspace project/labels or company default; Jira workspace site/project/issue type. Missing destinations return a typed error; do not guess routing or create a different integration.
110
+ - The result has `created` and `export` with issue key/URL. An existing sent or linked ticket returns `created: false`; an active export is a conflict. After ambiguous transport/provider failure, inspect `apex-finding-tickets` and provider state before retrying. Do not automatically repeat ticket creation.
111
+ - Ticket content uses the same title, severity, finding details, and Apex link as the frontend.
112
+
113
+ ## Existing Linear and Jira tickets
114
+
115
+ - After the company connects its integration, use `apex-finding-link-ticket` with `findingRef`, `provider: "linear" | "jira"`, and `issueRef` (ticket key or HTTPS issue URL). Local MCP accepts optional `cwd` and `scanId` and uses device login. A finding UUID works without a local binding; an identifier requires the bound workspace.
116
+ - Use `apex-finding-tickets` to read existing links. CLI fallback: `apex findings link-ticket <finding-ref> <linear|jira> <issue-key|url> --json`, then `apex findings tickets <finding-ref> --json`.
117
+ - Hosted MCP uses `findingId` (UUID) instead of `findingRef`, with no `cwd` or `scanId`. Linking requires explicit `findings:write`; listing requires `findings:read`. Default service-key scopes remain scans:create, scans:read, findings:read. Do not infer write permission from read access.
118
+ - Apex verifies the ticket through the connected integration and records the frontend link; it does not create or modify a provider ticket. Repeating the same association is safe. A different existing link or in-progress export is a conflict: do not replace it or create another ticket as a fallback. For ambiguous Jira sites, use the full issue URL.
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Cantina agent plugins for security review workflows.",
9
- "version": "0.1.28"
9
+ "version": "0.1.34"
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.28"
17
+ "version": "0.1.34"
18
18
  },
19
19
  "description": "Run Apex security scans and review findings from Claude Code.",
20
- "version": "0.1.28",
20
+ "version": "0.1.34",
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.28",
3
+ "version": "0.1.34",
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.28",
3
+ "version": "0.1.34",
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.28",
8
+ "@cantinasecurity/apex-cli@0.1.34",
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.28",
7
+ "@cantinasecurity/apex-cli@0.1.34",
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.28 apex-mcp
16
+ npx -y -p @cantinasecurity/apex-cli@0.1.34 apex-mcp
17
17
  ```
18
18
 
19
19
  That keeps marketplace installs independent of a user's global `apex` install.
package/README.md CHANGED
@@ -93,13 +93,19 @@ APEX_BASE_URL=https://bedrock.customer.example pnpm apex
93
93
 
94
94
  ## Try Apex With A Trial Code
95
95
 
96
+ Shared Standard-scan codes (including custom names such as `TEST100`) work with
97
+ Apex CLI 0.1.34 or newer. They provide either one guest Standard scan or 100% off
98
+ one 200-credit checkout package. Both paths share the code's redemption limit;
99
+ a one-use code cannot fund both. Previously issued `APEX-...` and `SPBT-...` codes
100
+ remain accepted. Code eligibility and remaining uses are checked by Bedrock.
101
+
96
102
  A Cantina-issued trial code can start a temporary guest scan without a Cantina
97
103
  account, browser, or device login. Redeem it from the repository root, run the
98
104
  normal scan lifecycle, and create a Cantina account only after reviewing the
99
105
  findings:
100
106
 
101
107
  ```bash
102
- apex redeem SPBT-DEVREL-XXXXXXXX --json
108
+ apex redeem <code> --json
103
109
  apex scan --non-interactive --json
104
110
  apex status --json
105
111
  apex findings --json
@@ -152,30 +158,30 @@ unclaimed local trial proof so `apex claim` can still use it.
152
158
  Agent-safe copy/paste prompt (substitute the issued code):
153
159
 
154
160
  ```text
155
- Install and run Cantina Apex CLI 0.1.28 to start one free standard security scan of this repository immediately.
161
+ Install and run Cantina Apex CLI 0.1.34 to start one free standard security scan of this repository immediately.
156
162
 
157
163
  Use this trial code only where <TRIAL_CODE> appears below: SPBT-DEVREL-XXXXXXXX
158
164
 
159
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`.
160
166
 
161
167
  Use this exact command prefix for every Apex command:
162
- npx -y -p @cantinasecurity/apex-cli@0.1.28 apex
168
+ npx -y -p @cantinasecurity/apex-cli@0.1.34 apex
163
169
 
164
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.
165
171
  2. Redeem, with at most one response-loss recovery retry:
166
- npx -y -p @cantinasecurity/apex-cli@0.1.28 apex redeem <TRIAL_CODE> --json
172
+ npx -y -p @cantinasecurity/apex-cli@0.1.34 apex redeem <TRIAL_CODE> --json
167
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.
168
174
  3. Immediately start exactly one scan; do not wait for another instruction:
169
- npx -y -p @cantinasecurity/apex-cli@0.1.28 apex scan --mode standard --non-interactive --no-open --json
175
+ npx -y -p @cantinasecurity/apex-cli@0.1.34 apex scan --mode standard --non-interactive --no-open --json
170
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.
171
177
  4. About every 60 seconds, poll that exact scan:
172
- npx -y -p @cantinasecurity/apex-cli@0.1.28 apex status --scan <SCAN_ID> --json
178
+ npx -y -p @cantinasecurity/apex-cli@0.1.34 apex status --scan <SCAN_ID> --json
173
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.
174
180
  5. Fetch that exact scan's findings:
175
- npx -y -p @cantinasecurity/apex-cli@0.1.28 apex findings --scan <SCAN_ID> --json
181
+ npx -y -p @cantinasecurity/apex-cli@0.1.34 apex findings --scan <SCAN_ID> --json
176
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.
177
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:
178
- npx -y -p @cantinasecurity/apex-cli@0.1.28 apex claim
184
+ npx -y -p @cantinasecurity/apex-cli@0.1.34 apex claim
179
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.
180
186
 
181
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.
@@ -212,6 +218,9 @@ Supported shell commands:
212
218
  - `/scans`
213
219
  - `/findings [scan-id]`
214
220
  - `/findings workspace [filter...]`
221
+ - `/findings tickets <finding-ref>`
222
+ - `/findings send <finding-ref> <linear|jira>`
223
+ - `/findings link-ticket <finding-ref> <linear|jira> <issue-key|url>`
215
224
  - `/findings comment <finding-id|finding-identifier> <comment>`
216
225
  - `/findings feedback <finding-id|finding-identifier> valid [comment]`
217
226
  - `/findings feedback <finding-id|finding-identifier> invalid <false-positive|by-design|not-relevant> [comment]`
@@ -247,6 +256,9 @@ Supported shell commands:
247
256
  - `apex scans`
248
257
  - `apex findings [--scan <scan-id>]`
249
258
  - `apex findings --workspace [--company <handle>] [--workspace-ref <name|prefix|id>] [--finding-state all|open] [--filter <[!]facet:value>]`
259
+ - `apex findings tickets <finding-ref> [--scan <scan-id>] [--json]`
260
+ - `apex findings send <finding-ref> <linear|jira> [--scan <scan-id>] [--json]`
261
+ - `apex findings link-ticket <finding-ref> <linear|jira> <issue-key|url> [--scan <scan-id>] [--json]`
250
262
  - `apex findings comment <finding-id|finding-identifier> --content <markdown> [--parent-comment <comment-id>] [--scan <scan-id>]`
251
263
  - `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>]`
252
264
  - `apex findings fix-review <finding-id|finding-identifier> [--scan <scan-id>] [--review-target pull-request|repository-current] [--fix-pr-url <github-pr-url>]`
@@ -296,7 +308,7 @@ The CLI automatically uses your company when only one is available. If you belon
296
308
 
297
309
  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.
298
310
 
299
- 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 all three scopes. For a P1-only adapter that reads manually started scans, create a read-only key with `--scope scans:read,findings:read`.
311
+ 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`.
300
312
 
301
313
  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.
302
314
 
@@ -340,7 +352,7 @@ Claude Code can expand the same environment variable from `.mcp.json`:
340
352
  }
341
353
  ```
342
354
 
343
- The hosted server exposes `apex-credits`, `apex-workspaces`, `apex-scan`, `apex-status`, `apex-cancel-scan`, `apex-findings`, and `apex-workspace-findings`. It operates only on repositories and workspaces available to the service key's organization. Use local stdio MCP for device auth, local filesystem/archive access, provider connection, setup, comments, feedback, fix reviews, and trial flows.
355
+ The hosted server exposes `apex-credits`, `apex-workspaces`, `apex-scan`, `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. It operates only on repositories and workspaces available to the service key's organization. Use local stdio MCP for device auth, local filesystem/archive access, provider connection, setup, comments, feedback, fix reviews, and trial flows.
344
356
 
345
357
  ### Hosted REST API
346
358
 
@@ -423,6 +435,29 @@ The matching MCP flow is `apex-finding-fix-review` with optional `fixPrUrls`. Ag
423
435
 
424
436
  The matching CLI and MCP flows intentionally use the same device-login session so agents should not ask users to paste browser cookies or auth tokens.
425
437
 
438
+ Send a finding to Linear or Jira to create a ticket, just as in the frontend:
439
+
440
+ ```bash
441
+ apex findings send KERN2-25 linear --json
442
+ apex findings send KERN2-25 jira --json
443
+ ```
444
+
445
+ Connect the integration and configure its destination in Apex first. Sending uses the same finding title, details, severity, Apex link, and saved routing as the frontend: workspace Linear project/labels (or the company default), or the workspace Jira site/project/issue type. The response contains `created` and `export` with the ticket key and URL. Already sent or linked findings return their existing ticket with `created: false`; active exports return a conflict. If a send times out, inspect `apex findings tickets` and the provider before retrying; the CLI never automatically repeats an uncertain creation.
446
+
447
+ Local MCP exposes `apex-finding-send` with `findingRef`, `provider`, and optional `cwd`/`scanId`. Hosted MCP uses `findingId` (UUID) and `provider`, with explicit `findings:write`. REST clients call `POST /api/apex/v1/findings/{findingId}/issue-trackers/{provider}` with no body. Provider is `linear` or `jira`. These paths all reuse the frontend's ticket-creation service.
448
+
449
+ Link an existing ticket after connecting Linear or Jira in company settings:
450
+
451
+ ```bash
452
+ apex findings link-ticket KERN2-25 linear SEC-123 --json
453
+ apex findings link-ticket KERN2-25 jira https://acme.atlassian.net/browse/SEC-123 --json
454
+ apex findings tickets KERN2-25 --json
455
+ ```
456
+
457
+ Use a finding UUID directly, or resolve its identifier/URL from the bound workspace and optional `--scan`. Ticket keys and HTTPS issue URLs are accepted; Linear also accepts an issue UUID. When Jira has several accessible sites, use a full issue URL or configure the workspace destination. Apex verifies the ticket through the company's existing integration and uses the same link shown in the frontend. One ticket per provider can be linked to a finding. Repeating the same link is safe; a different existing link or an in-progress export returns a conflict. This operation does not create or modify provider tickets.
458
+
459
+ Local MCP exposes `apex-finding-link-ticket` with `findingRef`, `provider`, `issueRef`, and optional `cwd`/`scanId`; `apex-finding-tickets` lists links. Both use device login. Hosted MCP exposes the same tool names with `findingId` (the finding UUID), without local filesystem arguments. Hosted writes require the explicit `findings:write` scope; reads require `findings:read`. Existing keys and omitted-scope defaults retain the original three scopes. Create a dedicated key in your own terminal with `apex service-key create --name "Ticket linking" --scope findings:read,findings:write` and keep the secret in your secret manager. REST clients use `GET`/`POST /api/apex/v1/findings/{findingId}/tickets`; POST accepts `{"provider":"linear","issueRef":"SEC-123"}`.
460
+
426
461
  ## Local Source Scans
427
462
 
428
463
  `apex scan` now works against any local source root you point it at. By default, that source root is the current working directory:
@@ -447,6 +482,8 @@ Lite scans use `lite` as the scan mode and require the Lite Scan beta feature pl
447
482
 
448
483
  PR scans require exactly one provider-backed GitHub repository. If the current directory resolves to multiple sources, pass `--repo <path>` to select the one that contains the pull request.
449
484
 
485
+ CLI and MCP scan results use the public Bedrock `scanId`. Kernel execution IDs are internal implementation details and are neither returned nor accepted as scan aliases.
486
+
450
487
  ## LLM / MCP Usage
451
488
 
452
489
  Apex has two MCP transports:
@@ -560,6 +597,8 @@ The local stdio MCP server exposes Apex-specific tools for:
560
597
  - Lite scan start by calling `apex-scan` with `mode: "lite"`
561
598
  - PR scan start by calling `apex-scan` with `mode: "pr"` and `pullRequests`
562
599
  - finding comments and valid/invalid feedback with `apex-finding-comment` and `apex-finding-feedback`
600
+ - Create Linear/Jira tickets with `apex-finding-send` using the saved frontend destination
601
+ - Link existing Linear/Jira tickets with `apex-finding-link-ticket` and inspect links with `apex-finding-tickets`
563
602
  - Fix PR and current-repository fix review scans with `apex-finding-fix-review`, optionally after `apex-finding-feedback`
564
603
 
565
604
  Neither MCP surface exposes trial redemption, trial claim, or service-key creation, listing, or revocation as tools. When a user supplies a Cantina trial code, run `apex redeem <code>` in a terminal instead of starting device login or putting the code in an MCP call. Production MCP clients should connect to `https://ai.cantina.xyz/mcp`; HTTP applications can use the [hosted Apex REST API](https://ai.cantina.xyz/docs/apex-api) directly. Do not launch the local stdio server as an HTTP adapter.
@@ -624,7 +663,7 @@ Telemetry event posts do not include bearer tokens. The telemetry endpoint is `P
624
663
  {
625
664
  "schemaVersion": 1,
626
665
  "event": "apex.invocation.completed",
627
- "cliVersion": "0.1.28",
666
+ "cliVersion": "0.1.34",
628
667
  "invocation": {
629
668
  "surface": "mcp",
630
669
  "command": "apex-scan",
@@ -655,7 +694,7 @@ The npm package also includes marketplace-ready plugin artifacts:
655
694
  - `.claude-plugin/plugin.json` and `.mcp.claude.json` for Claude Code plugin installs
656
695
  - `.claude-plugin/marketplace.json` for a Claude marketplace entry backed by the public npm package
657
696
 
658
- These plugin installs launch the pinned npm package with `npx -y -p @cantinasecurity/apex-cli@0.1.28 apex-mcp`, so users do not need to install `apex` globally before enabling the plugin.
697
+ These plugin installs launch the pinned npm package with `npx -y -p @cantinasecurity/apex-cli@0.1.34 apex-mcp`, so users do not need to install `apex` globally before enabling the plugin.
659
698
 
660
699
  The repository also includes `.agents/plugins/marketplace.json` for local Codex marketplace testing from a checkout.
661
700
 
package/dist/apex.js CHANGED
@@ -1,21 +1,24 @@
1
+ import { commandStartThreatModel, commandThreatModel, hasAuditSetupBeta, threatModelExitCode, terminalText } from "./threat-model.js";
2
+ import { getMe } from "./auth.js";
1
3
  import { ApexApiClient, formatApiError } from "./api-client.js";
2
- import { parseArgs, validateCommandFlags } from "./args.js";
3
- import { commandCancelScan, commandClaim, commandConnect, commandCredits, commandDoctor, commandExportFindings, commandFindingComment, commandFindingFeedback, commandFindingFixReview, commandFindings, commandWorkspaceFindings, commandLogin, commandLogout, commandRedeem, commandScan, commandScans, commandServiceKeyCreate, commandServiceKeyList, commandServiceKeyRevoke, commandSetup, commandStatus, commandTelemetry, commandUpdate, commandWorkspace, commandWorkspaceUse, commandWorkspaces, getRedeemExitCode, } from "./commands.js";
4
- import { CLI_HELP_TEXT } from "./help.js";
4
+ import { isAuditSetupInvocation, parseArgs, validateCommandFlags } from "./args.js";
5
+ 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 { cliHelpText } from "./help.js";
5
7
  import { runMcpServer } from "./mcp.js";
6
8
  import { runInteractiveShell } from "./shell.js";
7
9
  import { createCliTelemetryInvocation, emitInvocationCompleted, emitInvocationStarted, withTelemetryContext, } from "./telemetry.js";
8
10
  import { maybePromptForUpdate } from "./update.js";
9
11
  async function dispatch(parsed) {
12
+ const client = new ApexApiClient();
10
13
  if (parsed.flags.help === true || parsed.command === "help") {
11
- process.stdout.write(CLI_HELP_TEXT);
14
+ const me = await getMe(client, { signal: AbortSignal.timeout(1_500) }).catch(() => null);
15
+ process.stdout.write(cliHelpText(hasAuditSetupBeta(me)));
12
16
  return;
13
17
  }
14
18
  if (parsed.command === "mcp") {
15
19
  await runMcpServer();
16
20
  return;
17
21
  }
18
- const client = new ApexApiClient();
19
22
  const cwd = process.cwd();
20
23
  if (await maybePromptForUpdate(parsed.flags, parsed.command)) {
21
24
  return;
@@ -25,7 +28,13 @@ async function dispatch(parsed) {
25
28
  await runInteractiveShell(client, cwd, parsed.flags);
26
29
  return;
27
30
  case "scan":
28
- await commandScan(client, cwd, parsed.flags);
31
+ if (parsed.flags.setup !== undefined)
32
+ await commandStartThreatModel(client, cwd, parsed.flags);
33
+ else
34
+ await commandScan(client, cwd, parsed.flags);
35
+ return;
36
+ case "threat-model":
37
+ await commandThreatModel(client, cwd, parsed.flags, parsed.subcommand);
29
38
  return;
30
39
  case "scans":
31
40
  await commandScans(client, cwd, parsed.flags);
@@ -94,6 +103,24 @@ async function dispatch(parsed) {
94
103
  await commandWorkspace(cwd, parsed.flags);
95
104
  return;
96
105
  case "findings":
106
+ if (parsed.subcommand === "send") {
107
+ if (parsed.args.length !== 2)
108
+ throw new Error("Usage: apex findings send <finding-ref> <linear|jira>");
109
+ await commandFindingSend(client, cwd, parsed.flags, parsed.args[0], parsed.args[1]);
110
+ return;
111
+ }
112
+ if (parsed.subcommand === "tickets") {
113
+ if (parsed.args.length !== 1)
114
+ throw new Error("Usage: apex findings tickets <finding-ref>");
115
+ await commandFindingTickets(client, cwd, parsed.flags, parsed.args[0]);
116
+ return;
117
+ }
118
+ if (parsed.subcommand === "link-ticket") {
119
+ if (parsed.args.length !== 3)
120
+ throw new Error("Usage: apex findings link-ticket <finding-ref> <linear|jira> <issue-key|url>");
121
+ await commandFindingLinkTicket(client, cwd, parsed.flags, parsed.args[0], parsed.args[1], parsed.args[2]);
122
+ return;
123
+ }
97
124
  if (parsed.subcommand === "comment") {
98
125
  const findingRef = parsed.args[0] ?? "";
99
126
  const content = parsed.args.slice(1).join(" ").trim() || String(parsed.flags.content ?? "");
@@ -152,8 +179,10 @@ async function dispatch(parsed) {
152
179
  throw new Error(`Unknown command: ${parsed.command}`);
153
180
  }
154
181
  }
182
+ let parsedInvocation = null;
155
183
  async function main() {
156
184
  const parsed = parseArgs(process.argv.slice(2));
185
+ parsedInvocation = parsed;
157
186
  validateCommandFlags(parsed);
158
187
  const invocation = createCliTelemetryInvocation(parsed);
159
188
  emitInvocationStarted(invocation);
@@ -184,6 +213,10 @@ main().catch((error) => {
184
213
  process.exitCode = 1;
185
214
  return;
186
215
  }
187
- process.stderr.write(`${formatApiError(error)}\n`);
188
- process.exitCode = 1;
216
+ const setupCommand = isAuditSetupInvocation(parsedInvocation);
217
+ if (setupCommand && process.argv.includes("--json"))
218
+ process.stdout.write(`${JSON.stringify({ error: formatApiError(error), exitCode: threatModelExitCode(error) })}\n`);
219
+ else
220
+ process.stderr.write(`${setupCommand ? terminalText(formatApiError(error)) : formatApiError(error)}\n`);
221
+ process.exitCode = setupCommand ? threatModelExitCode(error) : 1;
189
222
  });
package/dist/args.js CHANGED
@@ -1,4 +1,14 @@
1
1
  export const KNOWN_CLI_FLAG_NAMES = [
2
+ "setup",
3
+ "draft",
4
+ "file",
5
+ "sha256",
6
+ "request-id",
7
+ "wait",
8
+ "timeout",
9
+ "start-scan",
10
+ "no-feedback-context",
11
+ "attachment",
2
12
  "comment",
3
13
  "company",
4
14
  "content",
@@ -35,6 +45,7 @@ export const KNOWN_CLI_FLAG_NAMES = [
35
45
  ];
36
46
  const KNOWN_CLI_FLAGS = new Set(KNOWN_CLI_FLAG_NAMES);
37
47
  const BOOLEAN_FLAGS = new Set([
48
+ "wait", "start-scan", "no-feedback-context",
38
49
  "force",
39
50
  "non-interactive",
40
51
  "json",
@@ -44,6 +55,7 @@ const BOOLEAN_FLAGS = new Set([
44
55
  "help",
45
56
  ]);
46
57
  const REPEATABLE_VALUE_FLAGS = new Set([
58
+ "attachment",
47
59
  "repo",
48
60
  "pr",
49
61
  "pr-path",
@@ -96,10 +108,14 @@ export function validateCommandFlags(parsed) {
96
108
  const isWorkspaceFindings = parsed.command === "findings" &&
97
109
  parsed.subcommand === null &&
98
110
  parsed.flags.workspace === true;
99
- if (!isWorkspaceFindings) {
111
+ const isSetupScan = parsed.command === "scan" && parsed.flags.setup !== undefined;
112
+ if (!isWorkspaceFindings && !isSetupScan) {
100
113
  throw new Error("--workspace-ref is only supported with `apex findings --workspace`. To change the local workspace used by scans or exports, run `apex workspace use <workspace-ref>` first.");
101
114
  }
102
115
  }
116
+ export function isAuditSetupInvocation(parsed) {
117
+ return (parsed?.command === "threat-model" || parsed?.flags.setup !== undefined);
118
+ }
103
119
  export function isJsonMode(flags) {
104
120
  return flags.json === true;
105
121
  }
package/dist/auth.js CHANGED
@@ -78,9 +78,9 @@ async function waitForDeviceLoginCredentials(client, options) {
78
78
  expiresAt,
79
79
  };
80
80
  }
81
- export async function getMe(client) {
81
+ export async function getMe(client, options = {}) {
82
82
  try {
83
- return await client.request("/api/cli/v1/me");
83
+ return await client.request("/api/cli/v1/me", options);
84
84
  }
85
85
  catch (error) {
86
86
  if (error instanceof ApiError && error.status === 401) {
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.request(`/api/cli/v1/local-workspaces/${encodeURIComponent(workspaceId)}/scan`, {
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.request(`/api/cli/v1/local-workspaces/${encodeURIComponent(workspaceId)}/scan`, {
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
- return client.request(`/api/cli/v2/local-workspaces/${encodeURIComponent(resolvedWorkspaceId)}/scan`, {
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
- return requestFindingAppRoute(client, `/api/findings/${encodeURIComponent(findingId)}/fix-review`, {
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
  }