@0cloud/cli 0.1.0 → 0.2.0
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/README.md +10 -5
- package/dist/public/customer.js +42 -11
- package/dist/public/http.js +2 -0
- package/dist/public/public-cli.js +3 -2
- package/dist/public/workflow-commands.js +5 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,8 +19,8 @@ CI. `--endpoint` and `--org` override the saved service and workspace.
|
|
|
19
19
|
|
|
20
20
|
| Task | Command |
|
|
21
21
|
| --- | --- |
|
|
22
|
-
| Analyze
|
|
23
|
-
|
|
|
22
|
+
| Analyze repository source where enabled | `0cloud scan https://github.com/owner/repository` |
|
|
23
|
+
| Source upload where enabled | `0cloud scan ./src --whitebox --focus "Account access"` |
|
|
24
24
|
| Inspect connected codebases | `0cloud codebases list` or `codebases show <repo-or-id>` |
|
|
25
25
|
| Check saved scope and readiness | `0cloud tests readiness <repo-or-id>` |
|
|
26
26
|
| Start a saved-codebase security test | `0cloud tests start <repo-or-id> --idempotency-key <uuid>` |
|
|
@@ -38,19 +38,24 @@ repairs. Opening configuration or checking readiness does not start work.
|
|
|
38
38
|
## Source analysis
|
|
39
39
|
|
|
40
40
|
```sh
|
|
41
|
-
0cloud scan ./src
|
|
42
41
|
0cloud scan https://github.com/owner/repository
|
|
43
42
|
0cloud scan ./src --whitebox --focus "Authorization and tenant isolation"
|
|
44
43
|
```
|
|
45
44
|
|
|
45
|
+
Ordinary `scan` accepts an HTTPS repository URL. Local files require an explicit
|
|
46
|
+
`--whitebox` source-analysis request; the CLI rejects them before reading source
|
|
47
|
+
or requesting a price. It never switches workflows automatically.
|
|
48
|
+
|
|
46
49
|
Uploads exclude symlinks, `.env` files, dependency/build directories, invalid
|
|
47
50
|
UTF-8 and common binary formats. Limits: 300 files, 400 KB per file and 8 MB total.
|
|
48
51
|
Excluded paths are reported on stderr; exceeding total limits fails rather than
|
|
49
52
|
silently returning a partial submission. Review the files you select before upload.
|
|
50
53
|
|
|
51
54
|
`--whitebox` uses `/api/whitebox-scan`; `--focus` requires it. Repository URL
|
|
52
|
-
support depends on deployment configuration.
|
|
53
|
-
can
|
|
55
|
+
support depends on deployment configuration. The `/api/scan` route is repository-only, while fixed-credit or funded source
|
|
56
|
+
configuration can reject URL acquisition. Those deployments may require explicit
|
|
57
|
+
`--whitebox` file uploads or the connected `source-scans` workflow below. A rejected
|
|
58
|
+
URL is reported as an error without a fallback.
|
|
54
59
|
|
|
55
60
|
For deployments with queued source operations enabled:
|
|
56
61
|
|
package/dist/public/customer.js
CHANGED
|
@@ -7,12 +7,12 @@ import { UserError } from "./errors.js";
|
|
|
7
7
|
export function dashboardApiContext(ctx) {
|
|
8
8
|
return { ...ctx, endpoint: ctx.endpoint.replace(/\/orchestrator\/?$/, "") };
|
|
9
9
|
}
|
|
10
|
-
export async function customerRequest(opts, route, method = "GET", body) {
|
|
10
|
+
export async function customerRequest(opts, route, method = "GET", body, idempotencyKey) {
|
|
11
11
|
if (!/^\/[a-zA-Z0-9]/.test(route) || route.includes("\\") || route.includes("#") || /(^|\/)\.\.(\/|$)/.test(decodeURIComponent(route))) {
|
|
12
12
|
throw new UserError("Use a relative API path, for example /scan or /project-setup.");
|
|
13
13
|
}
|
|
14
14
|
const ctx = dashboardApiContext(await resolveContext(opts));
|
|
15
|
-
return request(ctx, route, { method, body });
|
|
15
|
+
return request(ctx, route, { method, body, idempotencyKey });
|
|
16
16
|
}
|
|
17
17
|
const SKIP = new Set([".git", "node_modules", "vendor", "dist", "build", ".next", "target", "__pycache__", ".env", ".env.local"]);
|
|
18
18
|
const BINARY = /\.(png|jpe?g|gif|webp|ico|pdf|zip|gz|tar|wasm|woff2?|ttf|eot|mp[34]|mov|exe|dll|so|dylib|bin)$/i;
|
|
@@ -73,31 +73,62 @@ export async function readSubmission(input) {
|
|
|
73
73
|
throw new UserError("No source files to scan.");
|
|
74
74
|
return { files, skipped };
|
|
75
75
|
}
|
|
76
|
+
/** Fail closed on missing, unauthorized, malformed or unavailable pricing APIs. */
|
|
77
|
+
export async function quoteProduct(opts, offering) {
|
|
78
|
+
const catalog = await customerRequest(opts, "/product-prices");
|
|
79
|
+
if (!catalog || typeof catalog.enabled !== "boolean")
|
|
80
|
+
throw new UserError("Invalid product price response; no work was submitted.");
|
|
81
|
+
if (!catalog.enabled)
|
|
82
|
+
return {};
|
|
83
|
+
if (!Array.isArray(catalog.prices))
|
|
84
|
+
throw new UserError("Invalid product price response; no work was submitted.");
|
|
85
|
+
const matches = catalog.prices.filter((p) => !!p && typeof p === "object" && p.offering === offering);
|
|
86
|
+
const price = matches[0];
|
|
87
|
+
if (matches.length !== 1 || !price || !Number.isSafeInteger(price.credits) || price.credits <= 0 || typeof price.priceVersion !== "string" || !price.priceVersion.trim()) {
|
|
88
|
+
throw new UserError(`No valid fixed-credit price for ${offering}; no work was submitted.`);
|
|
89
|
+
}
|
|
90
|
+
process.stderr.write(`Price: ${price.credits} credits (${offering}; version ${price.priceVersion}).\n`);
|
|
91
|
+
return { expectedPriceVersion: price.priceVersion };
|
|
92
|
+
}
|
|
93
|
+
function attemptKey(key = randomUUID()) {
|
|
94
|
+
if (!/^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(key))
|
|
95
|
+
throw new UserError("Idempotency key must be a UUID.");
|
|
96
|
+
process.stderr.write(`Attempt key: ${key}. Reuse it for the same request after a connection failure; do not automatically resubmit.\n`);
|
|
97
|
+
return key;
|
|
98
|
+
}
|
|
76
99
|
export async function scanSubmission(input, opts, options = {}) {
|
|
77
100
|
if (options.focus && !options.whitebox)
|
|
78
101
|
throw new UserError("--focus requires --whitebox.");
|
|
102
|
+
if (!options.whitebox && !/^https:\/\//i.test(input)) {
|
|
103
|
+
throw new UserError("Repository analysis requires an HTTPS repository URL. Source uploads require explicit --whitebox and an enabled deployment.");
|
|
104
|
+
}
|
|
105
|
+
const key = attemptKey(options.idempotencyKey);
|
|
79
106
|
const route = options.whitebox ? "/whitebox-scan" : "/scan";
|
|
80
107
|
const focus = options.focus ? { focus: options.focus } : {};
|
|
108
|
+
let body;
|
|
81
109
|
if (/^https:\/\//i.test(input))
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
110
|
+
body = options.whitebox ? { input, ...focus } : { repo_url: input };
|
|
111
|
+
else {
|
|
112
|
+
const { files, skipped } = await readSubmission(input);
|
|
113
|
+
if (skipped)
|
|
114
|
+
process.stderr.write(`Skipped ${skipped} binary, generated or excluded paths.\n`);
|
|
115
|
+
body = { files, ...focus };
|
|
116
|
+
}
|
|
117
|
+
const quote = await quoteProduct(opts, "upload");
|
|
118
|
+
return customerRequest(opts, route, "POST", { ...body, ...(quote.expectedPriceVersion !== undefined ? { expected_price_version: quote.expectedPriceVersion } : {}) }, key);
|
|
87
119
|
}
|
|
88
120
|
export async function readCodebase(target, opts) {
|
|
89
121
|
const field = /^[0-9a-f-]{36}$/i.test(target) ? "repository_id" : "target";
|
|
90
122
|
return await customerRequest(opts, `/project-setup?${field}=${encodeURIComponent(target)}`);
|
|
91
123
|
}
|
|
92
124
|
export async function startPentest(target, opts, idempotencyKey = randomUUID()) {
|
|
93
|
-
|
|
94
|
-
throw new UserError("Idempotency key must be a UUID.");
|
|
95
|
-
process.stderr.write(`Attempt key: ${idempotencyKey}. Reuse it for the same request after a connection failure; do not automatically resubmit.\n`);
|
|
125
|
+
idempotencyKey = attemptKey(idempotencyKey);
|
|
96
126
|
const record = await readCodebase(target, opts);
|
|
97
127
|
if (!record.readiness.canStart)
|
|
98
128
|
throw new UserError(record.readiness.reason || "Save and approve the codebase configuration before starting a pentest.");
|
|
129
|
+
const quote = await quoteProduct(opts, record.plan?.productTier ?? "quick");
|
|
99
130
|
return customerRequest(opts, "/project-setup", "POST", {
|
|
100
|
-
action: "start", repositoryId: record.repository.id,
|
|
131
|
+
...quote, action: "start", repositoryId: record.repository.id,
|
|
101
132
|
expectedRevision: record.revision, idempotencyKey,
|
|
102
133
|
});
|
|
103
134
|
}
|
package/dist/public/http.js
CHANGED
|
@@ -32,6 +32,8 @@ export async function request(ctx, pathAndQuery, options = {}) {
|
|
|
32
32
|
if (!options.omitOrgHeader && ctx.orgId) {
|
|
33
33
|
headers["X-0-Org-Id"] = ctx.orgId;
|
|
34
34
|
}
|
|
35
|
+
if (options.idempotencyKey)
|
|
36
|
+
headers["Idempotency-Key"] = options.idempotencyKey;
|
|
35
37
|
let body;
|
|
36
38
|
if (options.body !== undefined) {
|
|
37
39
|
headers["Content-Type"] = "application/json";
|
|
@@ -42,7 +42,8 @@ export function buildPublicProgram() {
|
|
|
42
42
|
.action(async (o, cmd) => runAuthLogin({ ...context(cmd), ...o }));
|
|
43
43
|
auth.command("status").action(async (_o, cmd) => runAuthStatus({ json: true }));
|
|
44
44
|
auth.command("logout").action(runAuthLogout);
|
|
45
|
-
p.command("scan <
|
|
45
|
+
p.command("scan <repository-url-or-source>").description("Analyze a repository URL; --whitebox also accepts source uploads where enabled")
|
|
46
|
+
.option("--idempotency-key <uuid>", "Reuse this key when retrying the same source submission")
|
|
46
47
|
.option("--whitebox", "Deeper source-only analysis, not an active security test")
|
|
47
48
|
.option("--focus <text>", "Focus instructions for whitebox analysis")
|
|
48
49
|
.action(async (input, o, cmd) => output(await scanSubmission(input, context(cmd), o)));
|
|
@@ -121,7 +122,7 @@ export function buildPublicProgram() {
|
|
|
121
122
|
p.addHelpText("after", "\nStart here:\n 0cloud auth login\n 0cloud codebases list\n 0cloud tests readiness <codebase>\n 0cloud tests start <codebase> --idempotency-key <uuid>\n\nscan analyzes source; tests starts saved-codebase execution and honors its saved publication policy.\n");
|
|
122
123
|
p.command("capabilities").description("Print command and API coverage")
|
|
123
124
|
.action(() => output({ version: USER_AGENT_VERSION, commands: p.commands.map(c => c.name()),
|
|
124
|
-
workflows: { tests: "Saved-codebase security tests; pentests is a compatibility command", scan: "Immediate source-only analysis", sourceScans: "Queued exact-commit source analysis", memory: "Rules through the current Memory API", usage: "Read-only funding readiness and model catalog" },
|
|
125
|
+
workflows: { tests: "Saved-codebase security tests; pentests is a compatibility command", scan: "Immediate source-only analysis", sourceScans: "Queued exact-commit source analysis", memory: "Rules through the current Memory API", usage: "Read-only funding readiness, fixed product prices and model catalog" },
|
|
125
126
|
pending: ["Website target setup and authorized test-start API parity", "Manual PR review dispatch", "Knowledge documents public API", "Team, integration and payment administration"],
|
|
126
127
|
note: "Server availability and workspace permissions determine which actions can run." }));
|
|
127
128
|
return p;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Option } from "commander";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { setTimeout as delay } from "node:timers/promises";
|
|
4
|
-
import { customerRequest, readCodebase, startPentest } from "./customer.js";
|
|
4
|
+
import { customerRequest, quoteProduct, readCodebase, startPentest } from "./customer.js";
|
|
5
5
|
import { resolveContext } from "./config.js";
|
|
6
6
|
import { UserError } from "./errors.js";
|
|
7
7
|
const output = (value) => { process.stdout.write(JSON.stringify(value, null, 2) + "\n"); };
|
|
@@ -38,6 +38,8 @@ export function registerWorkflowCommands(p) {
|
|
|
38
38
|
const usage = p.command("usage").description("Read funding readiness and available inference models; never changes billing");
|
|
39
39
|
usage.command("status").action(async (_o, cmd) => output(await customerRequest(context(cmd), "/inference/account")));
|
|
40
40
|
usage.command("models").action(async (_o, cmd) => output(await customerRequest(context(cmd), "/inference/v1/models")));
|
|
41
|
+
usage.command("prices").description("Read fixed product credit prices for this workspace")
|
|
42
|
+
.action(async (_o, cmd) => output(await customerRequest(context(cmd), "/product-prices")));
|
|
41
43
|
const memory = p.command("memory").description("Read Memory and manage explicit rules with revision checks");
|
|
42
44
|
memory.command("list").option("--repository <uuid>", "Scope to a codebase", uuid)
|
|
43
45
|
.action(async (o, cmd) => output(await customerRequest(context(cmd), `/memory${o.repository ? `?repositoryId=${o.repository}` : ""}`)));
|
|
@@ -67,7 +69,8 @@ export function registerWorkflowCommands(p) {
|
|
|
67
69
|
const ctx = await resolveContext(context(cmd));
|
|
68
70
|
const idempotencyKey = o.idempotencyKey ?? randomUUID();
|
|
69
71
|
process.stderr.write(`Attempt key: ${idempotencyKey}. Reuse with the same repository, commit, operation and focus after a connection failure.\n`);
|
|
70
|
-
|
|
72
|
+
const quote = await quoteProduct(context(cmd), "upload");
|
|
73
|
+
output(await customerRequest(context(cmd), "/source-scans", "POST", { ...quote, orgId: ctx.orgId, repositoryId, commitSha: o.commit, operation: o.operation, idempotencyKey, ...(o.focus !== undefined ? { focus: o.focus } : {}) }));
|
|
71
74
|
});
|
|
72
75
|
async function read(id, cmd) {
|
|
73
76
|
const ctx = await resolveContext(context(cmd));
|