@cantinasecurity/apex-cli 0.1.35 → 0.1.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/apex-cli/SKILL.md +40 -0
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.mcp.claude.json +1 -1
- package/.mcp.codex.json +1 -1
- package/MARKETPLACE.md +1 -1
- package/README.md +61 -11
- package/dist/apex.js +6 -0
- package/dist/api-client.js +19 -9
- package/dist/args.js +12 -0
- package/dist/commands.js +53 -34
- package/dist/cvss.js +49 -0
- package/dist/help.js +11 -4
- package/dist/local-source-scan.js +25 -5
- package/dist/mcp.js +4 -2
- package/dist/repo-discovery.js +69 -16
- package/dist/session.js +24 -4
- package/dist/shell.js +7 -6
- package/dist/threat-model.js +21 -3
- package/package.json +9 -8
- package/skills/apex-cli/SKILL.md +40 -0
|
@@ -120,3 +120,43 @@ If the Apex MCP server is not configured, fall back to the local CLI:
|
|
|
120
120
|
- 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`.
|
|
121
121
|
- 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.
|
|
122
122
|
- 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.
|
|
123
|
+
|
|
124
|
+
### Workspace CVSS configuration
|
|
125
|
+
|
|
126
|
+
Use `apex workspace use <workspace>` to bind the intended existing workspace, then
|
|
127
|
+
`apex cvss get` to read its configuration and CVSS 3.1 policy template. To update it,
|
|
128
|
+
write a JSON file containing exactly `enabled` (boolean) and `policy` (the edited
|
|
129
|
+
template), then run `apex cvss set --file <configuration.json>`. Both commands emit
|
|
130
|
+
JSON and require an existing device-login account; they never initiate login,
|
|
131
|
+
create a workspace, or start a scan. Guest sessions are denied. `enabled: false`
|
|
132
|
+
disables CVSS scoring while preserving the supplied policy. This configures future
|
|
133
|
+
synthesis; it does not rescore historical findings.
|
|
134
|
+
|
|
135
|
+
CVSS administration is intentionally scripted CLI only for this release. No shell,
|
|
136
|
+
local MCP, hosted MCP, or organization service-key tool is added. Agents can invoke
|
|
137
|
+
these explicit terminal commands. Server deployment requires Bedrock CVSS API support for CLI bearer authentication.
|
|
138
|
+
|
|
139
|
+
Example (jq is used only to prepare the JSON file):
|
|
140
|
+
|
|
141
|
+
```sh
|
|
142
|
+
apex login
|
|
143
|
+
apex workspace use "<workspace name or prefix>"
|
|
144
|
+
apex cvss get > cvss-current.json
|
|
145
|
+
jq '.configuration // {enabled: true, policy: .template}' cvss-current.json > cvss.json
|
|
146
|
+
# Edit cvss.json, then save and verify:
|
|
147
|
+
apex cvss set --file cvss.json
|
|
148
|
+
apex cvss get
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Keep `policy.version` at `3.1`. Supply a name, instructions, and all eight metrics
|
|
152
|
+
(AV, AC, PR, UI, S, C, I, A). Each metric has an `options` map with at least one
|
|
153
|
+
allowed value and nonempty guidance for each allowed value. Omit disallowed
|
|
154
|
+
values. If Audit Setup or another workspace update is active, wait for it to
|
|
155
|
+
finish before saving again.
|
|
156
|
+
|
|
157
|
+
Scan target and recovery rules:
|
|
158
|
+
- CLI `apex scan [target...]` and repeatable `--repo` accept exact files, directories, or HTTPS GitHub/GitLab repository URLs; local MCP uses `repoPaths` for the same selections. No target means `cwd`.
|
|
159
|
+
- A file selection scans only that file, respecting ignore rules. Local snapshots exclude `.apex/` state. Remote URLs use the default branch and cannot fall back to uploading `cwd`; clone locally to choose a branch or commit.
|
|
160
|
+
- Use Standard for local changes, files, plain directories, and public repositories without provider access. Lite and Audit still require connected provider sources; do not silently substitute another mode.
|
|
161
|
+
- Unauthenticated headless CLI commands fail with explicit login guidance; they never start device login. Use the MCP auth tools for explicit MCP login. Retry transient refresh failures without clearing credentials.
|
|
162
|
+
- Audit Setup is an explicit Standard/Audit beta flow using saved workspace repositories and context. It does not preserve local targets. Never automatically substitute setup after `AUDIT_SETUP_REQUIRED`; use the error's selected-workspace command only when that saved scope is intended. Draft operations require the ID returned by preparation; eligible accounts receive setup commands in CLI help. Lite and PR do not use `--setup`.
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Cantina agent plugins for security review workflows.",
|
|
9
|
-
"version": "0.1.
|
|
9
|
+
"version": "0.1.37"
|
|
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.
|
|
17
|
+
"version": "0.1.37"
|
|
18
18
|
},
|
|
19
19
|
"description": "Run Apex security scans and review findings from Claude Code.",
|
|
20
|
-
"version": "0.1.
|
|
20
|
+
"version": "0.1.37",
|
|
21
21
|
"author": {
|
|
22
22
|
"name": "Cantina",
|
|
23
23
|
"email": "support@cantina.xyz"
|
package/.mcp.claude.json
CHANGED
package/.mcp.codex.json
CHANGED
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.
|
|
16
|
+
npx -y -p @cantinasecurity/apex-cli@0.1.37 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.
|
|
161
|
+
Install and run Cantina Apex CLI 0.1.37 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.
|
|
168
|
+
npx -y -p @cantinasecurity/apex-cli@0.1.37 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.
|
|
172
|
+
npx -y -p @cantinasecurity/apex-cli@0.1.37 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.
|
|
175
|
+
npx -y -p @cantinasecurity/apex-cli@0.1.37 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.
|
|
178
|
+
npx -y -p @cantinasecurity/apex-cli@0.1.37 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.
|
|
181
|
+
npx -y -p @cantinasecurity/apex-cli@0.1.37 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.
|
|
184
|
+
npx -y -p @cantinasecurity/apex-cli@0.1.37 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.
|
|
@@ -266,6 +266,8 @@ Supported shell commands:
|
|
|
266
266
|
- `apex workspaces`
|
|
267
267
|
- `apex workspace`
|
|
268
268
|
- `apex workspace use <workspace-name|workspace-prefix|workspace-id>`
|
|
269
|
+
- `apex cvss get`
|
|
270
|
+
- `apex cvss set --file <configuration.json>`
|
|
269
271
|
- `apex cancel-scan [scan-id]`
|
|
270
272
|
- `apex status [--scan <scan-id>]`
|
|
271
273
|
- `apex doctor`
|
|
@@ -464,7 +466,18 @@ Local MCP exposes `apex-finding-link-ticket` with `findingRef`, `provider`, `iss
|
|
|
464
466
|
|
|
465
467
|
## Local Source Scans
|
|
466
468
|
|
|
467
|
-
`apex scan`
|
|
469
|
+
`apex scan [target...]` accepts files, directories, and HTTPS GitHub/GitLab repository URLs. With no targets, it scans the current working directory:
|
|
470
|
+
|
|
471
|
+
```bash
|
|
472
|
+
apex scan . --mode standard
|
|
473
|
+
apex scan ./contracts --mode standard
|
|
474
|
+
apex scan ./contracts/UniswapV2ERC20.sol --mode standard
|
|
475
|
+
apex scan https://github.com/Uniswap/v2-core --mode standard
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
A file target uploads only that file, never its siblings. Local snapshots include current local changes and honor Git ignore rules and `.apexignore`; Apex state under `.apex/` is excluded. Explicit remote URLs use the default branch and never fall back to uploading the current checkout. Use a local clone to scan a particular remote branch or commit.
|
|
479
|
+
|
|
480
|
+
Source planning:
|
|
468
481
|
|
|
469
482
|
- clean GitHub or GitLab checkouts can stay on the remote-materialization path
|
|
470
483
|
- dirty git worktrees fall back to a local snapshot upload by default
|
|
@@ -472,18 +485,22 @@ Local MCP exposes `apex-finding-link-ticket` with `findingRef`, `provider`, `iss
|
|
|
472
485
|
|
|
473
486
|
Useful flags:
|
|
474
487
|
|
|
475
|
-
- `--repo <path>`
|
|
488
|
+
- `--repo <path-or-url>` as a repeatable alternative to positional targets
|
|
476
489
|
- `--source-mode auto|remote|local` to control remote-first fallback behavior
|
|
477
490
|
- `--mode standard|audit|lite|pr` to choose the scan mode
|
|
478
491
|
- `--pr <number>` to select one or more GitHub pull requests for `--mode pr`
|
|
479
492
|
- `--pr <number:path,path>` or `--pr-path <path>` to limit a PR scan to changed paths
|
|
480
493
|
|
|
494
|
+
Unauthenticated `--json`, `--non-interactive`, and non-TTY commands fail immediately with `apex login` guidance. Run login explicitly before automation. Temporary refresh/network failures preserve credentials; retry after the service recovers. Provider connection commands return URLs without opening browsers in automation.
|
|
495
|
+
|
|
481
496
|
`auto` is the default. `remote` requires Apex to materialize from a remote repository. `local` forces a local snapshot upload even when a clean remote path is available.
|
|
482
497
|
|
|
483
498
|
Audit scans use `audit` as the scan mode and still require provider-backed GitHub or GitLab repositories that Apex can materialize remotely without a local snapshot fallback. `ultra` remains accepted as a backwards-compatible alias.
|
|
484
499
|
|
|
485
500
|
Lite scans use `lite` as the scan mode and require the Lite Scan beta feature plus provider-backed GitHub or GitLab repositories that Apex can materialize remotely. Local snapshot uploads are not supported for Lite scans.
|
|
486
501
|
|
|
502
|
+
If a backend returns `AUDIT_SETUP_REQUIRED`, the error includes a setup command for the selected workspace. This explicit beta workflow uses the workspace's saved repositories and context; it does not preserve positional file or directory selections. Do not automatically substitute it for a local target scan. An explicit Standard source scan requires current CLI and backend versions. Eligible accounts receive setup commands in CLI help; draft operations use the ID returned by preparation. Lite and PR scans do not use `--setup`.
|
|
503
|
+
|
|
487
504
|
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.
|
|
488
505
|
|
|
489
506
|
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.
|
|
@@ -667,7 +684,7 @@ Telemetry event posts do not include bearer tokens. The telemetry endpoint is `P
|
|
|
667
684
|
{
|
|
668
685
|
"schemaVersion": 1,
|
|
669
686
|
"event": "apex.invocation.completed",
|
|
670
|
-
"cliVersion": "0.1.
|
|
687
|
+
"cliVersion": "0.1.37",
|
|
671
688
|
"invocation": {
|
|
672
689
|
"surface": "mcp",
|
|
673
690
|
"command": "apex-scan",
|
|
@@ -698,7 +715,7 @@ The npm package also includes marketplace-ready plugin artifacts:
|
|
|
698
715
|
- `.claude-plugin/plugin.json` and `.mcp.claude.json` for Claude Code plugin installs
|
|
699
716
|
- `.claude-plugin/marketplace.json` for a Claude marketplace entry backed by the public npm package
|
|
700
717
|
|
|
701
|
-
These plugin installs launch the pinned npm package with `npx -y -p @cantinasecurity/apex-cli@0.1.
|
|
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.
|
|
702
719
|
|
|
703
720
|
The repository also includes `.agents/plugins/marketplace.json` for local Codex marketplace testing from a checkout.
|
|
704
721
|
|
|
@@ -735,3 +752,36 @@ To move between existing Apex workspaces from the CLI:
|
|
|
735
752
|
3. If the workspace name contains spaces, quote it, for example `apex workspace use "Core Platform"`.
|
|
736
753
|
4. Use `apex scans`, `apex status --scan <scan-id>`, `apex findings`, and `apex export findings` against that binding.
|
|
737
754
|
5. Use `apex findings --workspace --company <handle> --workspace-ref <ref> --finding-state open` for the workspace browser's cross-scan findings that still need review, without changing the local binding.
|
|
755
|
+
|
|
756
|
+
### Workspace CVSS configuration
|
|
757
|
+
|
|
758
|
+
Use `apex workspace use <workspace>` to bind the intended existing workspace, then
|
|
759
|
+
`apex cvss get` to read its configuration and CVSS 3.1 policy template. To update it,
|
|
760
|
+
write a JSON file containing exactly `enabled` (boolean) and `policy` (the edited
|
|
761
|
+
template), then run `apex cvss set --file <configuration.json>`. Both commands emit
|
|
762
|
+
JSON and require an existing device-login account; they never initiate login,
|
|
763
|
+
create a workspace, or start a scan. Guest sessions are denied. `enabled: false`
|
|
764
|
+
disables CVSS scoring while preserving the supplied policy. This configures future
|
|
765
|
+
synthesis; it does not rescore historical findings.
|
|
766
|
+
|
|
767
|
+
CVSS administration is intentionally scripted CLI only for this release. No shell,
|
|
768
|
+
local MCP, hosted MCP, or organization service-key tool is added. Agents can invoke
|
|
769
|
+
these explicit terminal commands. Server deployment requires Bedrock CVSS API support for CLI bearer authentication.
|
|
770
|
+
|
|
771
|
+
Example (jq is used only to prepare the JSON file):
|
|
772
|
+
|
|
773
|
+
```sh
|
|
774
|
+
apex login
|
|
775
|
+
apex workspace use "<workspace name or prefix>"
|
|
776
|
+
apex cvss get > cvss-current.json
|
|
777
|
+
jq '.configuration // {enabled: true, policy: .template}' cvss-current.json > cvss.json
|
|
778
|
+
# Edit cvss.json, then save and verify:
|
|
779
|
+
apex cvss set --file cvss.json
|
|
780
|
+
apex cvss get
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
Keep `policy.version` at `3.1`. Supply a name, instructions, and all eight metrics
|
|
784
|
+
(AV, AC, PR, UI, S, C, I, A). Each metric has an `options` map with at least one
|
|
785
|
+
allowed value and nonempty guidance for each allowed value. Omit disallowed
|
|
786
|
+
values. If Audit Setup or another workspace update is active, wait for it to
|
|
787
|
+
finish before saving again.
|
package/dist/apex.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { commandCvss } from "./cvss.js";
|
|
1
2
|
import { commandStartThreatModel, commandThreatModel, hasAuditSetupBeta, threatModelExitCode, terminalText } from "./threat-model.js";
|
|
2
3
|
import { getMe } from "./auth.js";
|
|
3
4
|
import { ApexApiClient, formatApiError } from "./api-client.js";
|
|
@@ -94,6 +95,11 @@ async function dispatch(parsed) {
|
|
|
94
95
|
case "workspaces":
|
|
95
96
|
await commandWorkspaces(client, cwd, parsed.flags);
|
|
96
97
|
return;
|
|
98
|
+
case "cvss":
|
|
99
|
+
if (parsed.args.length)
|
|
100
|
+
throw new Error("Unexpected CVSS arguments");
|
|
101
|
+
await commandCvss(client, cwd, parsed.flags, parsed.subcommand);
|
|
102
|
+
return;
|
|
97
103
|
case "workspace":
|
|
98
104
|
if (parsed.subcommand === "use") {
|
|
99
105
|
const workspaceRef = parsed.args.join(" ");
|
package/dist/api-client.js
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
1
2
|
import { bindCredentialIssuerOriginIfCurrent, clearCredentials, clearCredentialsIfRefreshTokenMatches, loadConfig, loadCredentials, saveRefreshedCredentialsIfCurrent, } from "./config.js";
|
|
2
3
|
import { getTelemetryRequestHeaders } from "./telemetry.js";
|
|
3
4
|
import { redactSensitiveText, redactSensitiveValue, } from "./redaction.js";
|
|
4
5
|
export { redactSensitiveText } from "./redaction.js";
|
|
5
6
|
const CREDENTIAL_GENERATION_CHANGED_MESSAGE = "Apex credentials changed while this request was refreshing. The newer credentials were kept; retry the command.";
|
|
6
7
|
const HOSTED_APEX_ORIGIN = "https://ai.cantina.xyz";
|
|
8
|
+
const REFRESH_CREDENTIALS_SCHEMA = z.object({
|
|
9
|
+
accessToken: z.string().trim().min(1),
|
|
10
|
+
refreshToken: z.string().trim().min(1),
|
|
11
|
+
accessTokenExpiresAt: z.string().datetime({ offset: true }),
|
|
12
|
+
refreshTokenExpiresAt: z.string().datetime({ offset: true }),
|
|
13
|
+
});
|
|
7
14
|
const CREDENTIAL_ORIGIN_MISMATCH_MESSAGE = "Refusing to send stored Apex credentials because their issuer origin does not match the configured APEX_BASE_URL. Restore the origin used to sign in, or run `apex logout` before signing in to this origin.";
|
|
8
15
|
export class CredentialGenerationChangedError extends Error {
|
|
9
16
|
constructor() {
|
|
@@ -211,7 +218,11 @@ export class ApexApiClient {
|
|
|
211
218
|
});
|
|
212
219
|
}
|
|
213
220
|
async clearCredentialsAfterUnauthorized(expectedRefreshToken, error) {
|
|
214
|
-
|
|
221
|
+
// Transport errors, rate limits and server failures do not invalidate a
|
|
222
|
+
// token pair. Keep it recoverable for the next command.
|
|
223
|
+
const credentialsInvalid = (error instanceof ApiError && error.status === 401) ||
|
|
224
|
+
isTerminalGuestSessionError(error);
|
|
225
|
+
if (!expectedRefreshToken || !credentialsInvalid) {
|
|
215
226
|
return;
|
|
216
227
|
}
|
|
217
228
|
await this.clearCredentialsForGeneration(expectedRefreshToken, error);
|
|
@@ -244,20 +255,19 @@ export class ApexApiClient {
|
|
|
244
255
|
const effectiveRefreshToken = refreshToken ?? existing?.refreshToken ?? null;
|
|
245
256
|
if (!effectiveRefreshToken)
|
|
246
257
|
return null;
|
|
247
|
-
const payload =
|
|
258
|
+
const payload = await this.request("/api/cli/v1/auth/refresh", {
|
|
248
259
|
method: "POST",
|
|
249
260
|
auth: false,
|
|
250
261
|
json: { refreshToken: effectiveRefreshToken },
|
|
251
262
|
retryOnUnauthorized: false,
|
|
252
263
|
telemetryAttribution: false,
|
|
253
264
|
requestBaseUrl: baseUrl,
|
|
254
|
-
})
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
};
|
|
265
|
+
});
|
|
266
|
+
const parsed = REFRESH_CREDENTIALS_SCHEMA.safeParse(payload);
|
|
267
|
+
if (!parsed.success) {
|
|
268
|
+
throw new Error("Apex returned an invalid credential refresh response. Your credentials were kept; retry the command.");
|
|
269
|
+
}
|
|
270
|
+
const nextCredentials = parsed.data;
|
|
261
271
|
const persisted = await saveRefreshedCredentialsIfCurrent(effectiveRefreshToken, nextCredentials);
|
|
262
272
|
if (!persisted.saved) {
|
|
263
273
|
throw new CredentialGenerationChangedError();
|
package/dist/args.js
CHANGED
|
@@ -94,6 +94,15 @@ export function parseArgs(argv) {
|
|
|
94
94
|
}
|
|
95
95
|
index += 1;
|
|
96
96
|
}
|
|
97
|
+
// Scan operands are source targets, never subcommands. Keep them on the
|
|
98
|
+
// same typed path as --repo so the CLI and MCP share source selection.
|
|
99
|
+
if (positional[0] === "scan" && positional.length > 1) {
|
|
100
|
+
if (flags.setup !== undefined) {
|
|
101
|
+
throw new Error("Audit Setup does not accept scan targets. Use --workspace-ref <workspace>.");
|
|
102
|
+
}
|
|
103
|
+
flags.repo = [...(flags.repo ?? []), ...positional.slice(1)];
|
|
104
|
+
positional.splice(1);
|
|
105
|
+
}
|
|
97
106
|
return {
|
|
98
107
|
command: positional[0] ?? null,
|
|
99
108
|
subcommand: positional[1] ?? null,
|
|
@@ -102,6 +111,9 @@ export function parseArgs(argv) {
|
|
|
102
111
|
};
|
|
103
112
|
}
|
|
104
113
|
export function validateCommandFlags(parsed) {
|
|
114
|
+
if (parsed.flags["source-mode"] !== undefined && !["auto", "remote", "local"].includes(String(parsed.flags["source-mode"]))) {
|
|
115
|
+
throw new Error("--source-mode must be auto, remote, or local.");
|
|
116
|
+
}
|
|
105
117
|
if (parsed.flags["workspace-ref"] === undefined) {
|
|
106
118
|
return;
|
|
107
119
|
}
|
package/dist/commands.js
CHANGED
|
@@ -332,6 +332,8 @@ function formatLiteScanBalance(scanBalance) {
|
|
|
332
332
|
return `Lite scan credits: ${detailParts.join(", ")}`;
|
|
333
333
|
}
|
|
334
334
|
function normalizeRequestedScanMode(value) {
|
|
335
|
+
if (value === null || value === "standard")
|
|
336
|
+
return "standard";
|
|
335
337
|
if (value === "ultra" || value === "audit") {
|
|
336
338
|
return "audit";
|
|
337
339
|
}
|
|
@@ -341,7 +343,7 @@ function normalizeRequestedScanMode(value) {
|
|
|
341
343
|
if (value === "lite") {
|
|
342
344
|
return "lite";
|
|
343
345
|
}
|
|
344
|
-
|
|
346
|
+
throw new Error("--mode must be standard, audit, ultra, lite, or pr.");
|
|
345
347
|
}
|
|
346
348
|
function getFlagValues(flags, key) {
|
|
347
349
|
const values = getFlagList(flags, key);
|
|
@@ -1074,7 +1076,7 @@ export async function commandConnect(client, cwd, flags, provider) {
|
|
|
1074
1076
|
if (!isJsonMode(flags)) {
|
|
1075
1077
|
logLine(`Connection URL for ${provider}: ${response.url}`, flags);
|
|
1076
1078
|
}
|
|
1077
|
-
if (flags["no-open"] !== true) {
|
|
1079
|
+
if (flags["no-open"] !== true && canPromptInteractively(flags)) {
|
|
1078
1080
|
await openInBrowser(response.url);
|
|
1079
1081
|
}
|
|
1080
1082
|
if (isJsonMode(flags)) {
|
|
@@ -1097,7 +1099,23 @@ export async function commandScan(client, cwd, flags) {
|
|
|
1097
1099
|
const me = await ensureAuthenticated(client, flags);
|
|
1098
1100
|
const authState = await getCommandAuthState(client, me);
|
|
1099
1101
|
setTelemetryAuthKind(authState.authKind);
|
|
1102
|
+
if (authState.authKind === "guest" && requestedMode !== "standard") {
|
|
1103
|
+
throw new Error("Guest trials support standard scans only. Use --mode standard, or run `apex claim` before choosing another mode.");
|
|
1104
|
+
}
|
|
1105
|
+
const guestLocalSourceGuidance = "Guest trials require local snapshot uploads. Clone remote repositories locally, then scan a local path without --source-mode remote.";
|
|
1106
|
+
if (authState.authKind === "guest" && getFlagString(flags, "source-mode") === "remote") {
|
|
1107
|
+
throw new Error(guestLocalSourceGuidance);
|
|
1108
|
+
}
|
|
1100
1109
|
let selection = await selectWorkspaceTarget(cwd, me, flags);
|
|
1110
|
+
if (authState.authKind === "guest" && selection.sources.some((source) => source.absolutePath === null)) {
|
|
1111
|
+
throw new Error(guestLocalSourceGuidance);
|
|
1112
|
+
}
|
|
1113
|
+
const requiresLocalSnapshot = selection.sourceMode === "local" ||
|
|
1114
|
+
selection.sources.some((source) => source.kind !== "git_candidate" ||
|
|
1115
|
+
(selection.sourceMode !== "remote" && source.dirty));
|
|
1116
|
+
if ((requestedMode === "audit" || requestedMode === "lite") && requiresLocalSnapshot) {
|
|
1117
|
+
throw new Error(`${requestedMode === "audit" ? "Audit" : "Lite"} scans require provider-backed repositories without local changes. Use --mode standard for files, directories, or local changes.`);
|
|
1118
|
+
}
|
|
1101
1119
|
let preparedArchives = null;
|
|
1102
1120
|
if (authState.authKind === "guest" && requestedMode === "standard") {
|
|
1103
1121
|
preparedArchives = await withLoadingIndicator("Preparing local source archive...", flags, () => prepareLocalSourceArchives({
|
|
@@ -1139,6 +1157,21 @@ export async function commandScan(client, cwd, flags) {
|
|
|
1139
1157
|
let workspaceId = resolvedWorkspaceId;
|
|
1140
1158
|
let scan;
|
|
1141
1159
|
let prScanSource = null;
|
|
1160
|
+
const requestScanStart = async (endpoint, json) => {
|
|
1161
|
+
try {
|
|
1162
|
+
return normalizeScanStartResponse(await client.request(endpoint, { method: "POST", json }));
|
|
1163
|
+
}
|
|
1164
|
+
catch (error) {
|
|
1165
|
+
if (error instanceof ApiError && getApiErrorCode(error) === "AUDIT_SETUP_REQUIRED") {
|
|
1166
|
+
const quote = (value) => `'${value.replace(/'/g, "'\\''")}'`;
|
|
1167
|
+
const mode = requestedMode === "audit" ? "audit" : "standard";
|
|
1168
|
+
const command = `apex scan --company ${quote(result.company.id)} --workspace-ref ${quote(workspaceId)} --mode ${mode} --setup guided`;
|
|
1169
|
+
const message = `This backend requires Audit Setup. For this workspace's saved repositories, run: ${command}. Setup uses saved workspace sources and does not preserve local target selections. An explicit Standard source scan requires an updated client and backend; contact support if it still returns AUDIT_SETUP_REQUIRED.`;
|
|
1170
|
+
throw new ApiError(message, error.status, { code: "AUDIT_SETUP_REQUIRED", error: message });
|
|
1171
|
+
}
|
|
1172
|
+
throw error;
|
|
1173
|
+
}
|
|
1174
|
+
};
|
|
1142
1175
|
if (requestedMode === "pr") {
|
|
1143
1176
|
let legacyResolve = await withLoadingIndicator("Preparing PR scan...", flags, () => resolveWorkspaceSelectionLegacy(client, selection, resolvedWorkspaceId));
|
|
1144
1177
|
if (!legacyResolve.workspaceId) {
|
|
@@ -1161,40 +1194,30 @@ export async function commandScan(client, cwd, flags) {
|
|
|
1161
1194
|
const prSource = resolvePrScanSource(legacyResolve.repositories);
|
|
1162
1195
|
ensurePrSelectionsMatchSource(prSelections, prSource);
|
|
1163
1196
|
prScanSource = prSource;
|
|
1164
|
-
scan = await withLoadingIndicator("Starting PR scan...", flags, () =>
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
pullRequests: prSelections,
|
|
1174
|
-
}),
|
|
1175
|
-
},
|
|
1176
|
-
})
|
|
1177
|
-
.then(normalizeScanStartResponse));
|
|
1197
|
+
scan = await withLoadingIndicator("Starting PR scan...", flags, () => requestScanStart(`/api/cli/v1/local-workspaces/${encodeURIComponent(workspaceId)}/scan`, {
|
|
1198
|
+
mode: "pr",
|
|
1199
|
+
scanType: "pr",
|
|
1200
|
+
force: forceRestart || flags.force === true,
|
|
1201
|
+
repositories: buildPrScanRepositoriesPayload({
|
|
1202
|
+
source: prSource,
|
|
1203
|
+
pullRequests: prSelections,
|
|
1204
|
+
}),
|
|
1205
|
+
}));
|
|
1178
1206
|
}
|
|
1179
1207
|
else if (requestedMode === "audit" || requestedMode === "lite") {
|
|
1180
1208
|
const modeLabel = requestedMode === "audit" ? "Audit" : "Lite";
|
|
1181
1209
|
if (!supportsLegacyRemoteFlow(result.resolve.plannedSources)) {
|
|
1182
|
-
throw new Error(`${modeLabel} scans currently require provider-backed GitHub or GitLab repositories without local snapshot fallbacks.`);
|
|
1210
|
+
throw new Error(`${modeLabel} scans currently require provider-backed GitHub or GitLab repositories without local snapshot fallbacks. Use --mode standard for local snapshots or public repositories without a provider connection.`);
|
|
1183
1211
|
}
|
|
1184
1212
|
const legacyResolve = await withLoadingIndicator(`Preparing ${modeLabel.toLowerCase()} scan...`, flags, () => resolveWorkspaceSelectionLegacy(client, selection, resolvedWorkspaceId));
|
|
1185
1213
|
if (!legacyResolve.workspaceId) {
|
|
1186
1214
|
throw new Error("Workspace resolution did not return a workspaceId.");
|
|
1187
1215
|
}
|
|
1188
1216
|
workspaceId = legacyResolve.workspaceId;
|
|
1189
|
-
scan = await withLoadingIndicator(`Starting ${modeLabel.toLowerCase()} scan...`, flags, () =>
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
mode: requestedMode,
|
|
1194
|
-
force: forceRestart || flags.force === true,
|
|
1195
|
-
},
|
|
1196
|
-
})
|
|
1197
|
-
.then(normalizeScanStartResponse));
|
|
1217
|
+
scan = await withLoadingIndicator(`Starting ${modeLabel.toLowerCase()} scan...`, flags, () => requestScanStart(`/api/cli/v1/local-workspaces/${encodeURIComponent(workspaceId)}/scan`, {
|
|
1218
|
+
mode: requestedMode,
|
|
1219
|
+
force: forceRestart || flags.force === true,
|
|
1220
|
+
}));
|
|
1198
1221
|
}
|
|
1199
1222
|
else {
|
|
1200
1223
|
try {
|
|
@@ -1206,15 +1229,11 @@ export async function commandScan(client, cwd, flags) {
|
|
|
1206
1229
|
detectedSources: result.sources,
|
|
1207
1230
|
preparedArchives,
|
|
1208
1231
|
});
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
force: forceRestart || flags.force === true,
|
|
1214
|
-
scanSources,
|
|
1215
|
-
},
|
|
1232
|
+
return requestScanStart(`/api/cli/v2/local-workspaces/${encodeURIComponent(resolvedWorkspaceId)}/scan`, {
|
|
1233
|
+
mode: requestedMode,
|
|
1234
|
+
force: forceRestart || flags.force === true,
|
|
1235
|
+
scanSources,
|
|
1216
1236
|
});
|
|
1217
|
-
return normalizeScanStartResponse(response);
|
|
1218
1237
|
});
|
|
1219
1238
|
}
|
|
1220
1239
|
finally {
|
package/dist/cvss.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { getMe } from "./auth.js";
|
|
5
|
+
import { getFlagString } from "./args.js";
|
|
6
|
+
import { printJson } from "./output.js";
|
|
7
|
+
import { loadWorkspaceBinding } from "./workspace-binding.js";
|
|
8
|
+
const inputSchema = z
|
|
9
|
+
.object({
|
|
10
|
+
enabled: z.boolean(),
|
|
11
|
+
policy: z.record(z.string(), z.unknown()),
|
|
12
|
+
})
|
|
13
|
+
.strict();
|
|
14
|
+
export async function commandCvss(client, cwd, flags, action) {
|
|
15
|
+
const file = getFlagString(flags, "file");
|
|
16
|
+
if (!["get", "set"].includes(action ?? "") ||
|
|
17
|
+
(action === "set" ? !file : file))
|
|
18
|
+
throw new Error("Usage: apex cvss get | apex cvss set --file <configuration.json>");
|
|
19
|
+
if (flags.company || flags["workspace-name"] || flags["workspace-ref"])
|
|
20
|
+
throw new Error("CVSS uses the bound workspace. Run `apex workspace use <workspace>` first.");
|
|
21
|
+
const binding = await loadWorkspaceBinding(cwd);
|
|
22
|
+
if (!binding)
|
|
23
|
+
throw new Error("Run `apex workspace use <workspace>` before configuring CVSS.");
|
|
24
|
+
let input;
|
|
25
|
+
if (action === "set") {
|
|
26
|
+
const raw = await readFile(path.resolve(cwd, file), "utf8");
|
|
27
|
+
if (Buffer.byteLength(raw) > 128 * 1024)
|
|
28
|
+
throw new Error("CVSS configuration is too large.");
|
|
29
|
+
input = inputSchema.parse(JSON.parse(raw));
|
|
30
|
+
}
|
|
31
|
+
const me = await getMe(client);
|
|
32
|
+
if (!me)
|
|
33
|
+
throw new Error("Run `apex login` before configuring CVSS.");
|
|
34
|
+
if ((await client.getCredentials())?.guest ||
|
|
35
|
+
me.user.role?.trim().toLowerCase() === "guest")
|
|
36
|
+
throw new Error("CVSS configuration requires a signed-in account.");
|
|
37
|
+
const response = await client.request(`/api/workspaces/${encodeURIComponent(binding.workspaceId)}/cvss`, action === "set" ? { method: "PUT", json: input } : {});
|
|
38
|
+
const payload = {
|
|
39
|
+
configuration: response.configuration
|
|
40
|
+
? {
|
|
41
|
+
enabled: response.configuration.enabled,
|
|
42
|
+
policy: response.configuration.policy,
|
|
43
|
+
}
|
|
44
|
+
: null,
|
|
45
|
+
template: response.template,
|
|
46
|
+
};
|
|
47
|
+
printJson(payload);
|
|
48
|
+
return payload;
|
|
49
|
+
}
|
package/dist/help.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const CLI_HELP_TEXT = `Usage:
|
|
2
2
|
apex Open the interactive Apex shell
|
|
3
3
|
apex credits Show standard, audit, fix review, and Lite scan entitlements for the active company
|
|
4
|
-
apex scan
|
|
4
|
+
apex scan [target...] Create or resolve a workspace for this directory and start a scan
|
|
5
5
|
apex scans List scans for the current workspace binding
|
|
6
6
|
apex findings List findings for the latest or selected scan
|
|
7
7
|
apex findings --workspace List findings across the workspace browser scan set
|
|
@@ -18,6 +18,8 @@ export const CLI_HELP_TEXT = `Usage:
|
|
|
18
18
|
Start a fix review scan for a Fix PR or current repository
|
|
19
19
|
apex export findings Export findings for the latest or selected scan
|
|
20
20
|
apex workspaces List accessible workspaces for the active company
|
|
21
|
+
apex cvss get Read workspace CVSS configuration and template (JSON)
|
|
22
|
+
apex cvss set --file <json> Update the bound workspace CVSS 3.1 rubric
|
|
21
23
|
apex workspace Show the workspace currently bound to this directory
|
|
22
24
|
apex workspace use <workspace-name|workspace-prefix|workspace-id>
|
|
23
25
|
Bind this directory to an existing Apex workspace
|
|
@@ -73,7 +75,7 @@ Flags:
|
|
|
73
75
|
--source-mode auto|remote|local Control remote-vs-local source materialization
|
|
74
76
|
--force Start an overlapping non-PR scan, or confirm destructive guest logout
|
|
75
77
|
--replace Revoke and replace an existing guest trial session
|
|
76
|
-
--repo <path>
|
|
78
|
+
--repo <path-or-url> Select a file, directory, or HTTPS repository (repeatable)
|
|
77
79
|
--json Print machine-readable JSON output
|
|
78
80
|
--no-open Do not open browser flows automatically
|
|
79
81
|
--non-interactive Disable prompts and browser-dependent UX
|
|
@@ -81,7 +83,9 @@ Flags:
|
|
|
81
83
|
|
|
82
84
|
Tips:
|
|
83
85
|
apex scan uses the current directory name as the default workspace name unless you pass --workspace-name.
|
|
84
|
-
apex scan
|
|
86
|
+
apex scan defaults to the current directory; positional targets or --repo select files, directories, or HTTPS GitHub/GitLab repositories.
|
|
87
|
+
A file target scans only that file. Use --mode standard for local changes and public repositories without a provider connection.
|
|
88
|
+
Headless and --json commands require an existing session; run apex login explicitly first.
|
|
85
89
|
PR scans require --mode pr and at least one --pr selection for a GitHub repository.
|
|
86
90
|
audit is the current scan mode for audit scans; ultra remains accepted as a legacy alias.
|
|
87
91
|
Lite scans use --mode lite and require beta access plus provider-backed GitHub or GitLab sources.
|
|
@@ -130,6 +134,9 @@ Audit Setup beta:
|
|
|
130
134
|
--no-feedback-context Exclude prior feedback from this setup
|
|
131
135
|
--wait Wait for the next actionable setup state
|
|
132
136
|
--timeout <seconds> Bound next --wait (default 60, maximum 3600)
|
|
137
|
+
Setup uses saved workspace repositories and context, not local target selections.
|
|
138
|
+
Use the draft ID returned by setup: apex threat-model status --draft <uuid>.
|
|
139
|
+
Lite and PR scans do not use --setup.
|
|
133
140
|
`;
|
|
134
141
|
export function cliHelpText(auditSetupEnabled) {
|
|
135
142
|
return auditSetupEnabled
|
|
@@ -140,7 +147,7 @@ export const SHELL_HELP_TEXT = `Press Tab to autocomplete commands and common ar
|
|
|
140
147
|
|
|
141
148
|
Commands:
|
|
142
149
|
/credits Show standard, audit, fix review, and Lite scan entitlements for the active company
|
|
143
|
-
/scan [standard|audit|lite]
|
|
150
|
+
/scan [standard|audit|lite] [target...]
|
|
144
151
|
Start a new Apex scan for this workspace
|
|
145
152
|
/scan pr <pr-number> Start a PR scan for this workspace
|
|
146
153
|
/scans List scans for this workspace
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
3
|
import { createReadStream } from "node:fs";
|
|
4
|
-
import { lstat, mkdtemp, readFile, readdir, readlink, rm, stat, } from "node:fs/promises";
|
|
4
|
+
import { lstat, mkdtemp, readFile, readdir, readlink, realpath, rm, stat, } from "node:fs/promises";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { promisify } from "node:util";
|
|
@@ -11,6 +11,8 @@ const execFileAsync = promisify(execFile);
|
|
|
11
11
|
const MAX_ARCHIVE_ENTRIES = 100_000;
|
|
12
12
|
const MAX_GIT_OUTPUT_BYTES = 64 * 1024 * 1024;
|
|
13
13
|
const BUILTIN_IGNORE_PATTERNS = [
|
|
14
|
+
".apex",
|
|
15
|
+
".apex/**",
|
|
14
16
|
".git",
|
|
15
17
|
".git/**",
|
|
16
18
|
"node_modules",
|
|
@@ -89,6 +91,9 @@ async function buildIgnoreMatcher(rootPath) {
|
|
|
89
91
|
throw new Error(".apexignore is not valid UTF-8.");
|
|
90
92
|
}
|
|
91
93
|
matcher.add(decoded);
|
|
94
|
+
// Local CLI state is never scan input, even when a broad negation in the
|
|
95
|
+
// user's ignore file re-includes ordinary generated files.
|
|
96
|
+
matcher.add([".apex", ".apex/**"]);
|
|
92
97
|
}
|
|
93
98
|
catch (error) {
|
|
94
99
|
if (error.code !== "ENOENT") {
|
|
@@ -207,10 +212,19 @@ async function collectDirectoryEntries(rootPath, matcher) {
|
|
|
207
212
|
return entries;
|
|
208
213
|
}
|
|
209
214
|
async function collectArchiveEntries(source) {
|
|
215
|
+
if (!source.absolutePath) {
|
|
216
|
+
throw new Error("A remote repository URL cannot use a local snapshot. Connect its provider or clone it locally, then retry.");
|
|
217
|
+
}
|
|
218
|
+
const resolvedRoot = await realpath(source.absolutePath);
|
|
219
|
+
if (resolvedRoot.split(path.sep).some((segment) => segment.toLowerCase() === ".apex")) {
|
|
220
|
+
throw new Error("Apex local state (.apex) cannot be selected as scan input. Select your source files or repository instead.");
|
|
221
|
+
}
|
|
210
222
|
const matcher = await buildIgnoreMatcher(source.absolutePath);
|
|
211
223
|
const gitEntries = await collectGitEntries(source.absolutePath);
|
|
212
|
-
const
|
|
213
|
-
|
|
224
|
+
const selectedFiles = source.kind === "directory_candidate" ? source.selectedFiles : undefined;
|
|
225
|
+
const rawEntries = selectedFiles
|
|
226
|
+
? selectedFiles.filter((entry) => !gitEntries || gitEntries.includes(entry))
|
|
227
|
+
: gitEntries ?? await collectDirectoryEntries(source.absolutePath, matcher);
|
|
214
228
|
const fileEntries = new Set();
|
|
215
229
|
let uncompressedBytes = 0;
|
|
216
230
|
for (const entry of rawEntries.sort(compareArchivePaths)) {
|
|
@@ -305,7 +319,13 @@ async function inspectCreatedArchive(archivePath, sourceRoot, expectedFiles, dis
|
|
|
305
319
|
return { entryCount, uncompressedBytes };
|
|
306
320
|
}
|
|
307
321
|
async function createDeterministicArchive(params) {
|
|
322
|
+
const sourceRoot = params.source.absolutePath;
|
|
323
|
+
if (!sourceRoot)
|
|
324
|
+
throw new Error("Remote repository targets require remote materialization.");
|
|
308
325
|
const { files, uncompressedBytes } = await collectArchiveEntries(params.source);
|
|
326
|
+
if (files.length === 0) {
|
|
327
|
+
throw new Error(`No scannable files found in ${params.source.displayName}. Check the target and ignore rules.`);
|
|
328
|
+
}
|
|
309
329
|
if (files.length > MAX_ARCHIVE_ENTRIES) {
|
|
310
330
|
throw new Error(`Apex local snapshots support at most ${MAX_ARCHIVE_ENTRIES.toLocaleString()} entries per source.`);
|
|
311
331
|
}
|
|
@@ -313,7 +333,7 @@ async function createDeterministicArchive(params) {
|
|
|
313
333
|
throw new Error(`Archive for ${params.source.displayName} exceeds the uncompressed upload limit.`);
|
|
314
334
|
}
|
|
315
335
|
await tar.create({
|
|
316
|
-
cwd:
|
|
336
|
+
cwd: sourceRoot,
|
|
317
337
|
file: params.archivePath,
|
|
318
338
|
gzip: true,
|
|
319
339
|
portable: true,
|
|
@@ -334,7 +354,7 @@ async function createDeterministicArchive(params) {
|
|
|
334
354
|
entry.gname = "root";
|
|
335
355
|
},
|
|
336
356
|
}, files);
|
|
337
|
-
const observedArchive = await inspectCreatedArchive(params.archivePath,
|
|
357
|
+
const observedArchive = await inspectCreatedArchive(params.archivePath, sourceRoot, files, params.source.displayName, params.maxUncompressedBytes);
|
|
338
358
|
const manifest = await createArchiveManifest(params.archivePath);
|
|
339
359
|
return {
|
|
340
360
|
...manifest,
|
package/dist/mcp.js
CHANGED
|
@@ -27,7 +27,7 @@ Suggested workflow:
|
|
|
27
27
|
11. To review a fix, call apex-finding-fix-review. Pass fixPrUrls for PR-based fix reviews, or omit them to review the current repository state when Bedrock has original scan repository context.
|
|
28
28
|
|
|
29
29
|
Notes:
|
|
30
|
-
- Pass cwd explicitly for repository-specific operations.
|
|
30
|
+
- Pass cwd explicitly for repository-specific operations. repoPaths accepts exact files, directories, or HTTPS repository URLs; remote URLs never fall back to uploading cwd. Use Standard for local changes and unconnected public repositories.
|
|
31
31
|
- Tool calls are always non-interactive and never auto-open a browser.
|
|
32
32
|
- Finding write tools use the same Apex device-login credentials as read tools. Never ask for browser cookies or CANTINA_AUTH_TOKEN. An error requiring CANTINA_AUTH_TOKEN proves the MCP server is outdated; update Apex CLI, re-run apex setup for this client, restart it, and retry.
|
|
33
33
|
- Organization service-key creation, listing, and revocation are intentionally scripted-CLI-only and are not exposed as MCP tools. Never ask a user to paste a service key into a prompt or tool argument. A hosted MCP client supplies it only as the HTTP Authorization header.
|
|
@@ -42,6 +42,8 @@ const AUDIT_SETUP_MCP_GUIDE = `
|
|
|
42
42
|
|
|
43
43
|
Audit Setup beta:
|
|
44
44
|
- Use apex-scan-setup for a configured workspace, then apex-threat-model.
|
|
45
|
+
- Setup uses saved workspace repositories and context, not repoPaths or local file selections. Never substitute setup for an explicit source scan after AUDIT_SETUP_REQUIRED.
|
|
46
|
+
- Use the draft ID returned by setup for apex-threat-model; setup supports Standard and Audit, not Lite or PR.
|
|
45
47
|
- Read the complete model and get explicit approval before approve with the reviewed sha256 and startScan true.
|
|
46
48
|
- These tools use the local device account. Hosted service-key MCP does not expose Audit Setup.`;
|
|
47
49
|
function mcpWorkflowGuide(auditSetupEnabled) {
|
|
@@ -540,7 +542,7 @@ function registerTools(server, auditSetupEnabled = false) {
|
|
|
540
542
|
}, value => `Threat-model draft ${String(value.draftId)}: ${String(value.state)}.`, input));
|
|
541
543
|
server.registerTool("apex-scan", {
|
|
542
544
|
title: "Start Apex Scan",
|
|
543
|
-
description: "Start a new Apex scan for the provided cwd by default. Pass repoPaths
|
|
545
|
+
description: "Start a new Apex scan for the provided cwd by default. Pass repoPaths to select exact files, directories, or HTTPS GitHub/GitLab repository URLs. Remote URLs use the default branch; files never include siblings. Use mode audit for audit scans, mode lite for beta-gated provider-backed Lite scans, and mode pr for GitHub pull request scans.",
|
|
544
546
|
inputSchema: {
|
|
545
547
|
cwd: z.string().optional(),
|
|
546
548
|
company: z.string().optional(),
|
package/dist/repo-discovery.js
CHANGED
|
@@ -7,6 +7,8 @@ const execFileAsync = promisify(execFile);
|
|
|
7
7
|
async function runGit(cwd, args) {
|
|
8
8
|
const { stdout } = await execFileAsync("git", ["-C", cwd, ...args], {
|
|
9
9
|
encoding: "utf8",
|
|
10
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
11
|
+
timeout: 30_000,
|
|
10
12
|
});
|
|
11
13
|
return stdout.trim();
|
|
12
14
|
}
|
|
@@ -49,18 +51,39 @@ async function chooseRemote(directory) {
|
|
|
49
51
|
return firstFetch.url;
|
|
50
52
|
return remotes[0]?.url ?? null;
|
|
51
53
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
function inspectRemoteTarget(value) {
|
|
55
|
+
if (!/^(?:[a-z][a-z0-9+.-]*:\/\/|git@|(?:github|gitlab)\.com\/)/i.test(value))
|
|
56
|
+
return null;
|
|
57
|
+
const target = value.replace(/^(github|gitlab)\.com\//i, "https://$1.com/");
|
|
58
|
+
let url;
|
|
59
|
+
try {
|
|
60
|
+
url = new URL(target);
|
|
56
61
|
}
|
|
57
|
-
|
|
58
|
-
|
|
62
|
+
catch {
|
|
63
|
+
throw new Error("Remote scan targets must be HTTPS GitHub or GitLab repository URLs.");
|
|
59
64
|
}
|
|
60
|
-
|
|
61
|
-
|
|
65
|
+
const segments = url.pathname.replace(/\/+$/, "").split("/").filter(Boolean);
|
|
66
|
+
if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash || url.port ||
|
|
67
|
+
!["github.com", "gitlab.com"].includes(url.hostname) || segments.length < 2 ||
|
|
68
|
+
(url.hostname === "github.com" && segments.length !== 2) || segments.includes("-")) {
|
|
69
|
+
throw new Error("Remote scan targets must be HTTPS GitHub or GitLab repository URLs without credentials, query strings, or file/tree paths.");
|
|
62
70
|
}
|
|
63
|
-
|
|
71
|
+
const metadata = extractCanonicalRepoMetadata(url.href);
|
|
72
|
+
if (!metadata)
|
|
73
|
+
throw new Error("Unsupported remote repository target.");
|
|
74
|
+
return {
|
|
75
|
+
kind: "git_candidate",
|
|
76
|
+
path: `remote/${metadata.provider}/${segments.join("/").replace(/\.git$/i, "")}`,
|
|
77
|
+
absolutePath: null,
|
|
78
|
+
displayName: metadata.repoName,
|
|
79
|
+
repoUrl: metadata.repoUrl,
|
|
80
|
+
provider: metadata.provider,
|
|
81
|
+
branch: null,
|
|
82
|
+
commitSha: null,
|
|
83
|
+
dirty: false,
|
|
84
|
+
isRoot: false,
|
|
85
|
+
sourceType: "branch",
|
|
86
|
+
};
|
|
64
87
|
}
|
|
65
88
|
function normalizeSelectedRoots(cwd, selectedPaths) {
|
|
66
89
|
const resolved = selectedPaths
|
|
@@ -86,7 +109,7 @@ async function inspectGitRepository(rootCwd, directory) {
|
|
|
86
109
|
const metadata = remoteUrl ? extractCanonicalRepoMetadata(remoteUrl) : null;
|
|
87
110
|
const branchName = await runGit(directory, ["rev-parse", "--abbrev-ref", "HEAD"]).catch(() => "HEAD");
|
|
88
111
|
const commitSha = await runGit(directory, ["rev-parse", "HEAD"]).catch(() => null);
|
|
89
|
-
const dirtyOutput = await runGit(directory, ["status", "--porcelain"]).catch(() =>
|
|
112
|
+
const dirtyOutput = await runGit(directory, ["status", "--porcelain", "--", ".", ":(exclude).apex"]).catch(() => null);
|
|
90
113
|
const relativePath = path.relative(rootCwd, directory) || ".";
|
|
91
114
|
const detached = !branchName || branchName === "HEAD";
|
|
92
115
|
return {
|
|
@@ -98,13 +121,31 @@ async function inspectGitRepository(rootCwd, directory) {
|
|
|
98
121
|
provider: metadata?.provider ?? null,
|
|
99
122
|
branch: detached ? null : branchName,
|
|
100
123
|
commitSha: commitSha && commitSha.length > 0 ? commitSha : null,
|
|
101
|
-
|
|
124
|
+
// Unknown working-tree state must use a snapshot, never assume the remote
|
|
125
|
+
// commit contains the user's current files.
|
|
126
|
+
dirty: dirtyOutput === null || dirtyOutput.length > 0,
|
|
102
127
|
isRoot: relativePath === ".",
|
|
103
128
|
sourceType: detached ? "commit" : "branch",
|
|
104
129
|
};
|
|
105
130
|
}
|
|
106
131
|
async function inspectSource(rootCwd, requestedDirectory) {
|
|
107
|
-
const
|
|
132
|
+
const stats = await lstat(requestedDirectory).catch(() => null);
|
|
133
|
+
const requestedPath = path.relative(rootCwd, requestedDirectory) || ".";
|
|
134
|
+
if (!stats)
|
|
135
|
+
throw new Error(`Path does not exist: ${requestedPath}`);
|
|
136
|
+
if (stats.isFile()) {
|
|
137
|
+
return {
|
|
138
|
+
kind: "directory_candidate",
|
|
139
|
+
path: requestedPath,
|
|
140
|
+
absolutePath: path.dirname(requestedDirectory),
|
|
141
|
+
displayName: path.basename(requestedDirectory),
|
|
142
|
+
isRoot: false,
|
|
143
|
+
selectedFiles: [path.basename(requestedDirectory)],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (!stats.isDirectory())
|
|
147
|
+
throw new Error(`Path is not a regular file or directory: ${requestedPath}`);
|
|
148
|
+
const directory = requestedDirectory;
|
|
108
149
|
const relativePath = path.relative(rootCwd, directory) || ".";
|
|
109
150
|
const gitTopLevel = await getGitTopLevel(directory);
|
|
110
151
|
const comparableDirectory = await resolveComparablePath(directory);
|
|
@@ -120,10 +161,22 @@ async function inspectSource(rootCwd, requestedDirectory) {
|
|
|
120
161
|
};
|
|
121
162
|
}
|
|
122
163
|
export async function discoverSources(cwd, selectedPaths = []) {
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
164
|
+
const remoteSources = [];
|
|
165
|
+
const localPaths = [];
|
|
166
|
+
for (const target of selectedPaths) {
|
|
167
|
+
const remote = inspectRemoteTarget(target);
|
|
168
|
+
if (remote) {
|
|
169
|
+
if (!remoteSources.some((source) => source.path === remote.path))
|
|
170
|
+
remoteSources.push(remote);
|
|
171
|
+
}
|
|
172
|
+
else
|
|
173
|
+
localPaths.push(target);
|
|
174
|
+
}
|
|
175
|
+
const discoveredRoots = selectedPaths.length === 0 ? [cwd] : normalizeSelectedRoots(cwd, localPaths);
|
|
176
|
+
const sources = [...remoteSources, ...await Promise.all(discoveredRoots.map((root) => inspectSource(cwd, root)))];
|
|
177
|
+
if (new Set(sources.map((source) => source.path)).size !== sources.length) {
|
|
178
|
+
throw new Error("Scan targets have conflicting source paths. Select them in separate scans.");
|
|
179
|
+
}
|
|
127
180
|
sources.sort((left, right) => left.path.localeCompare(right.path));
|
|
128
181
|
return { sources, scanned: discoveredRoots };
|
|
129
182
|
}
|
package/dist/session.js
CHANGED
|
@@ -98,9 +98,16 @@ function selectBindingForWorkspaceName(flags, binding, workspaceName) {
|
|
|
98
98
|
}
|
|
99
99
|
function getSourceMode(flags) {
|
|
100
100
|
const value = getFlagString(flags, "source-mode");
|
|
101
|
-
|
|
101
|
+
if (value === null || value === "auto")
|
|
102
|
+
return "auto";
|
|
103
|
+
if (value === "remote" || value === "local")
|
|
104
|
+
return value;
|
|
105
|
+
throw new Error("--source-mode must be auto, remote, or local.");
|
|
102
106
|
}
|
|
103
107
|
function buildFallbackLocalArchivePlan(source, sourceMode) {
|
|
108
|
+
if (!source.absolutePath) {
|
|
109
|
+
throw new Error("A remote repository URL cannot use a local snapshot. Connect its provider or clone it locally, then retry.");
|
|
110
|
+
}
|
|
104
111
|
const reason = sourceMode === "local"
|
|
105
112
|
? "user_requested_local_snapshot"
|
|
106
113
|
: source.kind === "directory_candidate"
|
|
@@ -136,6 +143,11 @@ function buildFallbackLocalArchivePlan(source, sourceMode) {
|
|
|
136
143
|
}
|
|
137
144
|
function ensurePlannedSources(selection, resolve) {
|
|
138
145
|
if (resolve.plannedSources.length > 0) {
|
|
146
|
+
for (const source of selection.sources) {
|
|
147
|
+
if (!source.absolutePath && !resolve.plannedSources.some((plan) => plan.path === source.path && plan.sourceKind === "remote_repo")) {
|
|
148
|
+
throw new Error("The remote repository is not accessible. Connect its provider or clone it locally, then retry.");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
139
151
|
return resolve;
|
|
140
152
|
}
|
|
141
153
|
if (selection.sourceMode === "remote") {
|
|
@@ -172,6 +184,9 @@ export async function ensureAuthenticated(client, flags) {
|
|
|
172
184
|
if (isClaimProofAvailable(credentials)) {
|
|
173
185
|
throw new Error("Your guest trial session is no longer available, but its claim proof is stored on this machine. Run `apex claim` to keep the trial results.");
|
|
174
186
|
}
|
|
187
|
+
if (!canPromptInteractively(flags)) {
|
|
188
|
+
throw new Error("Apex is not authenticated. Run `apex login` explicitly, then retry this command.");
|
|
189
|
+
}
|
|
175
190
|
return login(client, {
|
|
176
191
|
noOpen: flags["no-open"] === true,
|
|
177
192
|
quiet: isJsonMode(flags),
|
|
@@ -182,16 +197,21 @@ export async function resolveWorkspace(client, cwd, me, flags) {
|
|
|
182
197
|
return resolveWorkspaceSelection(client, selection);
|
|
183
198
|
}
|
|
184
199
|
export async function selectWorkspaceTarget(cwd, me, flags) {
|
|
200
|
+
const sourceMode = getSourceMode(flags);
|
|
185
201
|
const loadedBinding = await loadWorkspaceBinding(cwd);
|
|
186
202
|
const company = await chooseCompany(me, flags, loadedBinding);
|
|
187
|
-
const
|
|
203
|
+
const companyBinding = loadedBinding?.companyId === company.id ? loadedBinding : null;
|
|
204
|
+
const workspaceNameSelection = await chooseWorkspaceName(cwd, flags, companyBinding);
|
|
188
205
|
const workspaceName = workspaceNameSelection.workspaceName;
|
|
189
|
-
const binding = selectBindingForWorkspaceName(flags,
|
|
206
|
+
const binding = selectBindingForWorkspaceName(flags, companyBinding, workspaceName);
|
|
190
207
|
const sourceSelection = getFlagList(flags, "repo");
|
|
191
208
|
const discovered = await discoverSources(cwd, sourceSelection);
|
|
192
209
|
if (discovered.sources.length === 0) {
|
|
193
210
|
throw new Error("No local sources were found.");
|
|
194
211
|
}
|
|
212
|
+
if (sourceMode === "local" && discovered.sources.some((source) => source.absolutePath === null)) {
|
|
213
|
+
throw new Error("Remote repository URLs cannot use --source-mode local. Clone the repository locally first.");
|
|
214
|
+
}
|
|
195
215
|
return {
|
|
196
216
|
company,
|
|
197
217
|
workspaceName,
|
|
@@ -199,7 +219,7 @@ export async function selectWorkspaceTarget(cwd, me, flags) {
|
|
|
199
219
|
binding,
|
|
200
220
|
sources: discovered.sources,
|
|
201
221
|
legacyRepositories: extractLegacyRepositories(discovered.sources),
|
|
202
|
-
sourceMode
|
|
222
|
+
sourceMode,
|
|
203
223
|
};
|
|
204
224
|
}
|
|
205
225
|
export async function resolveWorkspaceSelection(client, selection) {
|
package/dist/shell.js
CHANGED
|
@@ -206,11 +206,8 @@ async function runShellCommand(client, cwd, parsed, shellFlags, session) {
|
|
|
206
206
|
await commandClaim(client, cwd, shellFlags);
|
|
207
207
|
return false;
|
|
208
208
|
case "scan": {
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
process.stderr.write("Usage: /scan [standard|audit|ultra|lite|pr] [pr-number]\n");
|
|
212
|
-
return {};
|
|
213
|
-
}
|
|
209
|
+
const first = parsed.args[0];
|
|
210
|
+
const mode = first && ["standard", "audit", "ultra", "lite", "pr"].includes(first) ? first : null;
|
|
214
211
|
const prNumber = mode === "pr" ? parsed.args[1] : null;
|
|
215
212
|
if (mode === "pr" && !prNumber) {
|
|
216
213
|
process.stderr.write("Usage: /scan pr <pr-number>\n");
|
|
@@ -220,11 +217,15 @@ async function runShellCommand(client, cwd, parsed, shellFlags, session) {
|
|
|
220
217
|
process.stderr.write("Usage: /scan pr <pr-number>\n");
|
|
221
218
|
return {};
|
|
222
219
|
}
|
|
223
|
-
|
|
220
|
+
let commandFlags = mode === "pr" && prNumber
|
|
224
221
|
? withFlag(withFlag(shellFlags, "mode", mode), "pr", [prNumber])
|
|
225
222
|
: mode
|
|
226
223
|
? withFlag(shellFlags, "mode", mode)
|
|
227
224
|
: shellFlags;
|
|
225
|
+
const targets = parsed.args.slice(mode ? 1 : 0);
|
|
226
|
+
if (mode !== "pr" && targets.length > 0) {
|
|
227
|
+
commandFlags = withFlag(commandFlags, "repo", targets);
|
|
228
|
+
}
|
|
228
229
|
if (commandFlags.setup !== undefined) {
|
|
229
230
|
await commandStartThreatModel(client, cwd, commandFlags);
|
|
230
231
|
return {};
|
package/dist/threat-model.js
CHANGED
|
@@ -314,7 +314,18 @@ async function interactiveAnswers(state) {
|
|
|
314
314
|
};
|
|
315
315
|
}
|
|
316
316
|
export async function commandThreatModel(client, cwd, flags, action, supplied, emit = true) {
|
|
317
|
-
const
|
|
317
|
+
const selectedDraft = getFlagString(flags, "draft");
|
|
318
|
+
if (!selectedDraft) {
|
|
319
|
+
const binding = await loadWorkspaceBinding(cwd);
|
|
320
|
+
const setupCommand = binding
|
|
321
|
+
? "apex scan --mode standard --setup guided"
|
|
322
|
+
: "apex workspaces";
|
|
323
|
+
throw new ThreatModelUsageError(`Select --draft <id> from the setup response. To create a draft, run ${setupCommand}${binding ? "" : ", bind the intended workspace with apex workspace use <ref>, then run apex scan --mode standard --setup guided"}. Setup uses the workspace's saved repositories and context, not local file selections.`);
|
|
324
|
+
}
|
|
325
|
+
const parsedDraft = uuid.safeParse(selectedDraft);
|
|
326
|
+
if (!parsedDraft.success)
|
|
327
|
+
throw new ThreatModelUsageError("--draft must be a valid draft ID from the setup response.");
|
|
328
|
+
const draftId = parsedDraft.data;
|
|
318
329
|
requireAuditSetupBeta(await requireAccount(client));
|
|
319
330
|
let state;
|
|
320
331
|
if (action === "status" || action === "next") {
|
|
@@ -368,8 +379,15 @@ export async function commandThreatModel(client, cwd, flags, action, supplied, e
|
|
|
368
379
|
return state;
|
|
369
380
|
}
|
|
370
381
|
export async function commandStartThreatModel(client, cwd, flags, emit = true) {
|
|
371
|
-
const setup =
|
|
372
|
-
|
|
382
|
+
const setup = getFlagString(flags, "setup");
|
|
383
|
+
if (setup !== "guided" && setup !== "auto") {
|
|
384
|
+
throw new ThreatModelUsageError("--setup must be guided or auto.");
|
|
385
|
+
}
|
|
386
|
+
const requestedMode = getFlagString(flags, "mode");
|
|
387
|
+
const mode = requestedMode === "ultra" ? "audit" : requestedMode;
|
|
388
|
+
if (mode !== "standard" && mode !== "audit") {
|
|
389
|
+
throw new ThreatModelUsageError("Audit Setup supports --mode standard or --mode audit. Lite and PR scans do not use --setup; remove it and retry the intended scan mode.");
|
|
390
|
+
}
|
|
373
391
|
if (["repo", "pr", "pr-path", "source-mode", "force", "workspace-name"].some((key) => flags[key] !== undefined))
|
|
374
392
|
throw new ThreatModelUsageError("Audit Setup uses the configured workspace repositories and context; local source and force flags are not supported");
|
|
375
393
|
const me = await requireAccount(client);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cantinasecurity/apex-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.37",
|
|
4
4
|
"description": "Standalone CLI and MCP server for Apex.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -27,18 +27,19 @@
|
|
|
27
27
|
"node": ">=20"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
31
|
-
"ignore": "^7.0.
|
|
30
|
+
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
31
|
+
"ignore": "^7.0.12",
|
|
32
32
|
"proper-lockfile": "^4.1.2",
|
|
33
|
-
"tar": "^7.
|
|
34
|
-
"zod": "^4.
|
|
33
|
+
"tar": "^7.5.22",
|
|
34
|
+
"zod": "^4.6.5"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@types/node": "^22.
|
|
37
|
+
"@types/node": "^22.20.5",
|
|
38
38
|
"@types/proper-lockfile": "^4.1.4",
|
|
39
|
-
"tsx": "^4.
|
|
39
|
+
"tsx": "^4.23.15",
|
|
40
40
|
"typescript": "^5.9.3",
|
|
41
|
-
"
|
|
41
|
+
"vite": "^6.4.4",
|
|
42
|
+
"vitest": "^4.1.11"
|
|
42
43
|
},
|
|
43
44
|
"scripts": {
|
|
44
45
|
"apex": "tsx src/apex.ts",
|
package/skills/apex-cli/SKILL.md
CHANGED
|
@@ -120,3 +120,43 @@ verify `apex-credits` without printing the credential.
|
|
|
120
120
|
- 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`.
|
|
121
121
|
- 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.
|
|
122
122
|
- 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.
|
|
123
|
+
|
|
124
|
+
### Workspace CVSS configuration
|
|
125
|
+
|
|
126
|
+
Use `apex workspace use <workspace>` to bind the intended existing workspace, then
|
|
127
|
+
`apex cvss get` to read its configuration and CVSS 3.1 policy template. To update it,
|
|
128
|
+
write a JSON file containing exactly `enabled` (boolean) and `policy` (the edited
|
|
129
|
+
template), then run `apex cvss set --file <configuration.json>`. Both commands emit
|
|
130
|
+
JSON and require an existing device-login account; they never initiate login,
|
|
131
|
+
create a workspace, or start a scan. Guest sessions are denied. `enabled: false`
|
|
132
|
+
disables CVSS scoring while preserving the supplied policy. This configures future
|
|
133
|
+
synthesis; it does not rescore historical findings.
|
|
134
|
+
|
|
135
|
+
CVSS administration is intentionally scripted CLI only for this release. No shell,
|
|
136
|
+
local MCP, hosted MCP, or organization service-key tool is added. Agents can invoke
|
|
137
|
+
these explicit terminal commands. Server deployment requires Bedrock CVSS API support for CLI bearer authentication.
|
|
138
|
+
|
|
139
|
+
Example (jq is used only to prepare the JSON file):
|
|
140
|
+
|
|
141
|
+
```sh
|
|
142
|
+
apex login
|
|
143
|
+
apex workspace use "<workspace name or prefix>"
|
|
144
|
+
apex cvss get > cvss-current.json
|
|
145
|
+
jq '.configuration // {enabled: true, policy: .template}' cvss-current.json > cvss.json
|
|
146
|
+
# Edit cvss.json, then save and verify:
|
|
147
|
+
apex cvss set --file cvss.json
|
|
148
|
+
apex cvss get
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Keep `policy.version` at `3.1`. Supply a name, instructions, and all eight metrics
|
|
152
|
+
(AV, AC, PR, UI, S, C, I, A). Each metric has an `options` map with at least one
|
|
153
|
+
allowed value and nonempty guidance for each allowed value. Omit disallowed
|
|
154
|
+
values. If Audit Setup or another workspace update is active, wait for it to
|
|
155
|
+
finish before saving again.
|
|
156
|
+
|
|
157
|
+
Scan target and recovery rules:
|
|
158
|
+
- CLI `apex scan [target...]` and repeatable `--repo` accept exact files, directories, or HTTPS GitHub/GitLab repository URLs; local MCP uses `repoPaths` for the same selections. No target means `cwd`.
|
|
159
|
+
- A file selection scans only that file, respecting ignore rules. Local snapshots exclude `.apex/` state. Remote URLs use the default branch and cannot fall back to uploading `cwd`; clone locally to choose a branch or commit.
|
|
160
|
+
- Use Standard for local changes, files, plain directories, and public repositories without provider access. Lite and Audit still require connected provider sources; do not silently substitute another mode.
|
|
161
|
+
- Unauthenticated headless CLI commands fail with explicit login guidance; they never start device login. Use the MCP auth tools for explicit MCP login. Retry transient refresh failures without clearing credentials.
|
|
162
|
+
- Audit Setup is an explicit Standard/Audit beta flow using saved workspace repositories and context. It does not preserve local targets. Never automatically substitute setup after `AUDIT_SETUP_REQUIRED`; use the error's selected-workspace command only when that saved scope is intended. Draft operations require the ID returned by preparation; eligible accounts receive setup commands in CLI help. Lite and PR do not use `--setup`.
|