@murk-security/cli 0.4.0 → 0.5.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 +93 -0
- package/mcp-server.mjs +78 -13
- package/murk.mjs +92 -16
- package/package.json +3 -2
package/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# @murk-security/cli
|
|
2
|
+
|
|
3
|
+
Run [MURK](https://murk.sh)'s MCP server for your AI agent, and scan your
|
|
4
|
+
locally-running apps with MURK's **hosted** runner over a secure **outbound** tunnel.
|
|
5
|
+
|
|
6
|
+
- **No local runner, no model key** — scanning happens on MURK's infrastructure.
|
|
7
|
+
- **Nothing inbound** to your network. You choose exactly which `localhost` URLs are
|
|
8
|
+
exposed; this process forwards **only** those, **read-only** (GET/HEAD), and refuses
|
|
9
|
+
everything else. The allowlist is enforced on your machine — it's the trust boundary.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npm i -g @murk-security/cli
|
|
15
|
+
# or run without installing:
|
|
16
|
+
npx @murk-security/cli --help
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Requires Node ≥ 20.
|
|
20
|
+
|
|
21
|
+
## Quickstart
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
murk login # browser sign-in; authorizes this machine
|
|
25
|
+
murk add murk-security/northwind-store # add an app from a connected GitHub repo
|
|
26
|
+
murk apps # list your applications + ids
|
|
27
|
+
murk scan http://localhost:3000 # scan a locally-running app (full depth)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Use it from an AI agent (MCP)
|
|
31
|
+
|
|
32
|
+
**Hosted** (recommended) — no local process to keep running:
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
claude mcp add --transport http murk https://api.murk.sh/mcp
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Local** — also bridges any `--expose`'d local apps so the hosted runner can reach them:
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
murk mcp --expose http://localhost:3000
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Either way the agent gets these tools:
|
|
45
|
+
|
|
46
|
+
| Tool | What it does |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| `murk_list_applications` | List your apps (targets). |
|
|
49
|
+
| `murk_add_application` | Add an app from a connected GitHub repo. |
|
|
50
|
+
| `murk_scan` | Start a scan. Accepts an app **id or name**; `scanProfile: pr\|full`. |
|
|
51
|
+
| `murk_retest` | Re-verify known findings; fixed ones are closed. |
|
|
52
|
+
| `murk_create_patch` | Generate a code patch for a finding and open a PR. |
|
|
53
|
+
| `murk_list_findings` | List findings, filtered by app / status / severity. |
|
|
54
|
+
| `murk_get_finding` | One finding, with its reproduction recipe and any patch. |
|
|
55
|
+
| `murk_get_run` | A run and its findings — poll this for scan progress. |
|
|
56
|
+
|
|
57
|
+
Scans and patches run **asynchronously** — the start tools return a run id; poll
|
|
58
|
+
`murk_get_run` / `murk_get_finding` for results.
|
|
59
|
+
|
|
60
|
+
## Commands
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
murk login browser sign-in; authorizes this machine
|
|
64
|
+
murk logout revoke this machine's credential
|
|
65
|
+
murk whoami show login status
|
|
66
|
+
murk apps list your applications (id, name, source)
|
|
67
|
+
murk add <owner/repo> [--name NAME] add an app from a connected GitHub repo
|
|
68
|
+
murk scan <http://localhost:PORT> [--pr|--full] one-shot: connect, scan, disconnect
|
|
69
|
+
murk mcp [--expose <http://localhost:PORT> ...] run the MCP server for your agent
|
|
70
|
+
murk connect --expose <http://localhost:PORT> open the tunnel only (no MCP)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Scan depth
|
|
74
|
+
|
|
75
|
+
- `--full` — deep whole-repo audit with the model council (~45 min). **Default.**
|
|
76
|
+
- `--pr` — fast, cheaper scan, no council (~10 min).
|
|
77
|
+
|
|
78
|
+
## GitHub apps
|
|
79
|
+
|
|
80
|
+
`murk add owner/repo` requires the **MURK GitHub App** to be installed on that repository
|
|
81
|
+
(and granted access). Connect it from the MURK dashboard, then the repo becomes available
|
|
82
|
+
to `murk add` and `murk_add_application`.
|
|
83
|
+
|
|
84
|
+
## Environment
|
|
85
|
+
|
|
86
|
+
| Variable | Default | Purpose |
|
|
87
|
+
| --- | --- | --- |
|
|
88
|
+
| `MURK_API_URL` | `https://api.murk.sh` | API origin (set to `http://localhost:8788` for dev). |
|
|
89
|
+
| `MURK_DASHBOARD_URL` | `https://app.murk.sh` | Dashboard origin for login. |
|
|
90
|
+
| `MURK_API_TOKEN` | — | Org API token (`murk_…`) for CI; overrides the stored login. |
|
|
91
|
+
| `MURK_NO_BROWSER` | — | Set to print the login URL instead of opening a browser (headless/SSH). |
|
|
92
|
+
|
|
93
|
+
The login credential is stored at `~/.murk/credentials.json` (mode `600`).
|
package/mcp-server.mjs
CHANGED
|
@@ -33,17 +33,52 @@ const fail = (error) => {
|
|
|
33
33
|
};
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
+
/** Resolve an application by id, or by exact name (case-insensitive), or a unique name
|
|
37
|
+
* substring — so callers can pass a friendly name instead of an opaque id. Exported so the
|
|
38
|
+
* CLI and the MCP tools share one resolution rule. Throws a clear, actionable error. */
|
|
39
|
+
export async function resolveApplication(api, idOrName) {
|
|
40
|
+
const want = String(idOrName ?? '').trim();
|
|
41
|
+
if (!want) throw new Error('no application id or name given');
|
|
42
|
+
const apps = await api('GET', '/api/applications');
|
|
43
|
+
const list = Array.isArray(apps) ? apps : (apps.applications ?? []);
|
|
44
|
+
let app = list.find((a) => a.id === want)
|
|
45
|
+
|| list.find((a) => String(a.name).toLowerCase() === want.toLowerCase());
|
|
46
|
+
if (!app) {
|
|
47
|
+
const matches = list.filter((a) => String(a.name).toLowerCase().includes(want.toLowerCase()));
|
|
48
|
+
if (matches.length === 1) [app] = matches;
|
|
49
|
+
else if (matches.length > 1) {
|
|
50
|
+
throw new Error(`"${idOrName}" matches ${matches.length} applications (${matches.map((m) => m.name).join(', ')}); use the exact name or id.`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (!app) throw new Error(`no application matches "${idOrName}". List them with murk_list_applications (or \`murk apps\`).`);
|
|
54
|
+
return app;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Add an application from a connected GitHub repo (owner/name, grey-box source form).
|
|
58
|
+
* Exported so the CLI's `murk add` and the MCP `murk_add_application` tool share one path. */
|
|
59
|
+
export async function addGithubApplication(api, { githubRepo, name }) {
|
|
60
|
+
const want = String(githubRepo).trim().toLowerCase();
|
|
61
|
+
const conns = await api('GET', '/api/connections');
|
|
62
|
+
const active = (Array.isArray(conns) ? conns : conns.connections ?? []).filter((c) => c.status === 'active');
|
|
63
|
+
let match;
|
|
64
|
+
for (const c of active) {
|
|
65
|
+
const repos = await api('GET', `/api/connections/${encodeURIComponent(c.id)}/repositories`).catch(() => []);
|
|
66
|
+
const r = (Array.isArray(repos) ? repos : repos.repositories ?? []).find((x) => String(x.fullName).toLowerCase() === want);
|
|
67
|
+
if (r) { match = { connectionId: c.id, repositoryId: r.id, fullName: r.fullName }; break; }
|
|
68
|
+
}
|
|
69
|
+
if (!match) throw new Error(`No connected GitHub repository matches "${githubRepo}". Install the MURK GitHub app on that repo (and grant it access), then retry.`);
|
|
70
|
+
const asset = await api('POST', '/api/assets/github', { connectionId: match.connectionId, repositoryId: match.repositoryId });
|
|
71
|
+
return api('POST', '/api/applications', {
|
|
72
|
+
name: name || match.fullName, description: '', context: '',
|
|
73
|
+
sourceAssetId: asset.id, environments: [], identities: [],
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
36
77
|
/** Build the MURK MCP server with all tools, backed by the given authenticated `api`. */
|
|
37
78
|
export function buildMurkMcpServer(api, opts = {}) {
|
|
38
79
|
const RUNNER_MODE = opts.runnerMode ?? 'basic-harness';
|
|
39
80
|
|
|
40
|
-
|
|
41
|
-
const apps = await api('GET', '/api/applications');
|
|
42
|
-
const list = Array.isArray(apps) ? apps : (apps.applications ?? []);
|
|
43
|
-
const app = list.find((a) => a.id === applicationId);
|
|
44
|
-
if (!app) throw new Error(`application ${applicationId} not found`);
|
|
45
|
-
return app;
|
|
46
|
-
}
|
|
81
|
+
const getApplication = (idOrName) => resolveApplication(api, idOrName);
|
|
47
82
|
|
|
48
83
|
// Create -> preflight -> launch an assessment of the given kind (the flow the dashboard
|
|
49
84
|
// uses), returning the started run.
|
|
@@ -61,6 +96,7 @@ export function buildMurkMcpServer(api, opts = {}) {
|
|
|
61
96
|
policy: { profile: 'standard', destructiveActions: false, maxConcurrency: 2, maxDurationMinutes: 20 },
|
|
62
97
|
packs: ['surface', 'sast', 'intel', 'scan', 'authz'],
|
|
63
98
|
kind: o.kind,
|
|
99
|
+
...(o.scanProfile ? { scanProfile: o.scanProfile } : {}),
|
|
64
100
|
...(o.remediateFindingId ? { remediateFindingId: o.remediateFindingId } : {}),
|
|
65
101
|
};
|
|
66
102
|
const created = await api('POST', '/api/assessments', config);
|
|
@@ -82,11 +118,23 @@ export function buildMurkMcpServer(api, opts = {}) {
|
|
|
82
118
|
},
|
|
83
119
|
);
|
|
84
120
|
|
|
121
|
+
server.registerTool(
|
|
122
|
+
'murk_add_application',
|
|
123
|
+
{
|
|
124
|
+
title: 'Add an application',
|
|
125
|
+
description: 'Add an application from a connected GitHub repository (source-only, grey-box — the form factor for PR scans and patches). The MURK GitHub App must already be installed on the repo. Returns the created application.',
|
|
126
|
+
inputSchema: { githubRepo: z.string().describe('owner/name, e.g. murk-security/northwind-store'), name: z.string().optional() },
|
|
127
|
+
},
|
|
128
|
+
async ({ githubRepo, name }) => {
|
|
129
|
+
try { return ok(await addGithubApplication(api, { githubRepo, name })); } catch (e) { return fail(e); }
|
|
130
|
+
},
|
|
131
|
+
);
|
|
132
|
+
|
|
85
133
|
server.registerTool(
|
|
86
134
|
'murk_list_findings',
|
|
87
135
|
{
|
|
88
136
|
title: 'List findings',
|
|
89
|
-
description: 'List findings, optionally filtered by application, status (open/fixed/regressed/accepted) or severity.',
|
|
137
|
+
description: 'List findings, optionally filtered by application, status (open/in_progress/ready_for_retest/fixed/regressed/accepted) or severity.',
|
|
90
138
|
inputSchema: { applicationId: z.string().optional(), status: z.string().optional(), severity: z.string().optional() },
|
|
91
139
|
},
|
|
92
140
|
async ({ applicationId, status, severity }) => {
|
|
@@ -134,15 +182,32 @@ export function buildMurkMcpServer(api, opts = {}) {
|
|
|
134
182
|
|
|
135
183
|
server.registerTool(
|
|
136
184
|
'murk_scan',
|
|
137
|
-
{
|
|
138
|
-
|
|
139
|
-
|
|
185
|
+
{
|
|
186
|
+
title: 'Scan an application',
|
|
187
|
+
description: 'Start a security assessment of an application (public or locally-connected). Returns the started run; the scan runs asynchronously — poll murk_get_run with the returned run id for status and findings.',
|
|
188
|
+
inputSchema: {
|
|
189
|
+
applicationId: z.string().describe('Application id OR name (e.g. "northwind-store"); names are resolved for you.'),
|
|
190
|
+
scanProfile: z.enum(['pr', 'full']).optional().describe(
|
|
191
|
+
"Scan depth. 'full' (default): deep whole-repo audit with the model council (~45 min). " +
|
|
192
|
+
"'pr': fast, cheaper scan with no council (~10 min) — scoped to a pull-request diff when one " +
|
|
193
|
+
'is available, otherwise a light whole-repo pass.',
|
|
194
|
+
),
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
async ({ applicationId, scanProfile }) => {
|
|
198
|
+
try {
|
|
199
|
+
return ok(await launchAssessment(await getApplication(applicationId), {
|
|
200
|
+
kind: 'assess',
|
|
201
|
+
name: `Scan (via MCP${scanProfile ? `, ${scanProfile}` : ''})`,
|
|
202
|
+
scanProfile,
|
|
203
|
+
}));
|
|
204
|
+
} catch (e) { return fail(e); }
|
|
140
205
|
},
|
|
141
206
|
);
|
|
142
207
|
|
|
143
208
|
server.registerTool(
|
|
144
209
|
'murk_retest',
|
|
145
|
-
{ title: 'Retest findings', description: 'Re-verify an application’s known findings; findings that no longer reproduce are marked fixed.', inputSchema: { applicationId: z.string() } },
|
|
210
|
+
{ title: 'Retest findings', description: 'Re-verify an application’s known findings; findings that no longer reproduce are marked fixed.', inputSchema: { applicationId: z.string().describe('Application id OR name.') } },
|
|
146
211
|
async ({ applicationId }) => {
|
|
147
212
|
try { return ok(await launchAssessment(await getApplication(applicationId), { kind: 'retest', name: 'Retest (via MCP)' })); } catch (e) { return fail(e); }
|
|
148
213
|
},
|
|
@@ -150,7 +215,7 @@ export function buildMurkMcpServer(api, opts = {}) {
|
|
|
150
215
|
|
|
151
216
|
server.registerTool(
|
|
152
217
|
'murk_create_patch',
|
|
153
|
-
{ title: 'Create a patch', description: 'Generate a code patch for one finding (requires a connected source repo) and open a PR.', inputSchema: { findingId: z.string(), applicationId: z.string() } },
|
|
218
|
+
{ title: 'Create a patch', description: 'Generate a code patch for one finding (requires a connected source repo) and open a PR. The patch is generated asynchronously — poll murk_get_finding to see the diff and PR link when ready.', inputSchema: { findingId: z.string(), applicationId: z.string().describe('Application id OR name.') } },
|
|
154
219
|
async ({ findingId, applicationId }) => {
|
|
155
220
|
try {
|
|
156
221
|
return ok(await launchAssessment(await getApplication(applicationId), { kind: 'remediate', name: 'Patch (via MCP)', remediateFindingId: findingId }));
|
package/murk.mjs
CHANGED
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
* murk login # browser sign-in; authorizes this machine
|
|
9
9
|
* murk logout # revoke this machine's credential
|
|
10
10
|
* murk whoami # show login status
|
|
11
|
+
* murk apps # list your applications
|
|
12
|
+
* murk add owner/repo [--name NAME] # add an app from a connected GitHub repo
|
|
11
13
|
* murk mcp --expose http://localhost:3000 # MCP server for your agent + tunnel
|
|
12
|
-
* murk scan http://localhost:3000
|
|
14
|
+
* murk scan http://localhost:3000 [--pr|--full] # one-shot: connect, scan, disconnect
|
|
13
15
|
* murk connect --expose http://localhost:3000 # tunnel only (no MCP)
|
|
14
16
|
*
|
|
15
17
|
* Env:
|
|
@@ -173,7 +175,7 @@ async function cmdConnect(exposed) {
|
|
|
173
175
|
process.on('SIGTERM', stop);
|
|
174
176
|
}
|
|
175
177
|
|
|
176
|
-
async function launchScan(applicationId) {
|
|
178
|
+
async function launchScan(applicationId, scanProfile) {
|
|
177
179
|
const apps = await api('GET', '/api/applications');
|
|
178
180
|
const list = Array.isArray(apps) ? apps : (apps.applications ?? []);
|
|
179
181
|
const app = list.find((a) => a.id === applicationId);
|
|
@@ -184,6 +186,7 @@ async function launchScan(applicationId) {
|
|
|
184
186
|
target: { environmentAssetIds: assetIds }, scope: {}, identities: app.identities ?? [],
|
|
185
187
|
policy: { profile: 'standard', destructiveActions: false, maxConcurrency: 1, maxDurationMinutes: 15 },
|
|
186
188
|
packs: ['surface', 'sast', 'intel', 'scan', 'authz'], kind: 'assess',
|
|
189
|
+
...(scanProfile ? { scanProfile } : {}),
|
|
187
190
|
};
|
|
188
191
|
const created = await api('POST', '/api/assessments', config);
|
|
189
192
|
const pre = await api('POST', `/api/assessments/${created.id}/preflight`, { runnerMode: 'basic-harness' });
|
|
@@ -192,7 +195,7 @@ async function launchScan(applicationId) {
|
|
|
192
195
|
return run.runId ?? run.id ?? run.run?.id;
|
|
193
196
|
}
|
|
194
197
|
|
|
195
|
-
async function cmdScan(url) {
|
|
198
|
+
async function cmdScan(url, scanProfile) {
|
|
196
199
|
await ensureAuth();
|
|
197
200
|
preflightChecks([url]);
|
|
198
201
|
await ensureCanLaunch(); // fail fast + note the free trial before opening the tunnel
|
|
@@ -200,8 +203,8 @@ async function cmdScan(url) {
|
|
|
200
203
|
const ws = openTunnel([url], {
|
|
201
204
|
onReady: async (_u, applicationId) => {
|
|
202
205
|
try {
|
|
203
|
-
log(
|
|
204
|
-
const runId = await launchScan(applicationId);
|
|
206
|
+
log(`starting hosted ${scanProfile === 'pr' ? 'PR (fast)' : 'full'} scan ...`);
|
|
207
|
+
const runId = await launchScan(applicationId, scanProfile);
|
|
205
208
|
log(`run ${runId} started; watching ...`);
|
|
206
209
|
for (;;) {
|
|
207
210
|
await new Promise((r) => setTimeout(r, 4000));
|
|
@@ -345,6 +348,44 @@ async function cmdWhoami() {
|
|
|
345
348
|
} catch (e) { log(`could not reach ${API_URL}: ${e instanceof Error ? e.message : e}`); }
|
|
346
349
|
}
|
|
347
350
|
|
|
351
|
+
// Add a GitHub-connected application from the CLI (mirrors the MCP murk_add_application
|
|
352
|
+
// tool; shares addGithubApplication so the two never diverge). Logs go to stderr; the new
|
|
353
|
+
// application id is printed to stdout so it can be piped into scripts.
|
|
354
|
+
async function cmdAdd(repo, name) {
|
|
355
|
+
await ensureAuth();
|
|
356
|
+
if (!repo) die('usage: murk add <owner/repo> [--name NAME]');
|
|
357
|
+
const { addGithubApplication } = await import('./mcp-server.mjs');
|
|
358
|
+
const app = await addGithubApplication(api, { githubRepo: repo, name });
|
|
359
|
+
log(`added application "${app.name}" (${app.id})`);
|
|
360
|
+
process.stdout.write(`${app.id}\n`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// List the organization's applications (id, name, source kind) for discovery.
|
|
364
|
+
async function cmdApps() {
|
|
365
|
+
await ensureAuth();
|
|
366
|
+
const apps = await api('GET', '/api/applications');
|
|
367
|
+
const list = Array.isArray(apps) ? apps : (apps.applications ?? []);
|
|
368
|
+
if (!list.length) { log('no applications yet. Add one: murk add <owner/repo>'); return; }
|
|
369
|
+
for (const a of list) {
|
|
370
|
+
const src = a.sourceAssetId ? 'github' : ((a.environments ?? []).length ? 'env' : '-');
|
|
371
|
+
process.stdout.write(`${a.id} ${a.name} [${src}]\n`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// First positional arg that isn't a flag or a flag's value (for `murk add <repo>`).
|
|
376
|
+
function firstPositional(rest, valueFlags = ['--name']) {
|
|
377
|
+
for (let i = 0; i < rest.length; i++) {
|
|
378
|
+
if (valueFlags.includes(rest[i])) { i++; continue; }
|
|
379
|
+
if (!rest[i].startsWith('-')) return rest[i];
|
|
380
|
+
}
|
|
381
|
+
return undefined;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function parseFlagValue(rest, flag) {
|
|
385
|
+
const i = rest.indexOf(flag);
|
|
386
|
+
return i >= 0 && rest[i + 1] ? rest[i + 1] : undefined;
|
|
387
|
+
}
|
|
388
|
+
|
|
348
389
|
function parseExposed(rest) {
|
|
349
390
|
const exposed = [];
|
|
350
391
|
for (let i = 0; i < rest.length; i++) {
|
|
@@ -354,6 +395,43 @@ function parseExposed(rest) {
|
|
|
354
395
|
return exposed;
|
|
355
396
|
}
|
|
356
397
|
|
|
398
|
+
// Scan depth from flags: --pr / --full, or --profile pr|full. Default (undefined) => 'full'
|
|
399
|
+
// server-side. 'pr' is the fast, cheaper, no-council scan.
|
|
400
|
+
function parseScanProfile(rest) {
|
|
401
|
+
for (let i = 0; i < rest.length; i++) {
|
|
402
|
+
if (rest[i] === '--pr') return 'pr';
|
|
403
|
+
if (rest[i] === '--full') return 'full';
|
|
404
|
+
if (rest[i] === '--profile' && (rest[i + 1] === 'pr' || rest[i + 1] === 'full')) return rest[i + 1];
|
|
405
|
+
}
|
|
406
|
+
return undefined;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const USAGE =
|
|
410
|
+
'murk — MCP server for your AI agent + scan your local apps with MURK’s hosted runner.\n' +
|
|
411
|
+
'\n' +
|
|
412
|
+
'usage:\n' +
|
|
413
|
+
' murk login browser sign-in; authorizes this machine\n' +
|
|
414
|
+
' murk logout revoke this machine’s credential\n' +
|
|
415
|
+
' murk whoami show login status\n' +
|
|
416
|
+
' murk apps list your applications (id, name, source)\n' +
|
|
417
|
+
' murk add <owner/repo> [--name NAME] add an app from a connected GitHub repo\n' +
|
|
418
|
+
' murk scan <http://localhost:PORT> [--pr|--full] one-shot: connect, scan, disconnect (default: full)\n' +
|
|
419
|
+
' murk mcp [--expose <http://localhost:PORT> ...] run the MCP server for your agent; exposes local apps\n' +
|
|
420
|
+
' murk connect --expose <http://localhost:PORT> open the tunnel only (no MCP)\n' +
|
|
421
|
+
'\n' +
|
|
422
|
+
'scan depth:\n' +
|
|
423
|
+
' --full deep whole-repo audit with the model council (~45 min) [default]\n' +
|
|
424
|
+
' --pr fast, cheaper scan, no council (~10 min)\n' +
|
|
425
|
+
'\n' +
|
|
426
|
+
'env:\n' +
|
|
427
|
+
' MURK_API_URL API origin (default https://api.murk.sh)\n' +
|
|
428
|
+
' MURK_DASHBOARD_URL dashboard origin for login (default https://app.murk.sh)\n' +
|
|
429
|
+
' MURK_API_TOKEN org API token (murk_…) for CI; overrides the stored login\n' +
|
|
430
|
+
' MURK_NO_BROWSER set to print the login URL instead of opening a browser\n' +
|
|
431
|
+
'\n' +
|
|
432
|
+
'agents: add the hosted MCP with claude mcp add --transport http murk https://api.murk.sh/mcp\n' +
|
|
433
|
+
' or run it locally with murk mcp\n';
|
|
434
|
+
|
|
357
435
|
function main() {
|
|
358
436
|
const [cmd, ...rest] = process.argv.slice(2);
|
|
359
437
|
const fatal = (e) => die(e instanceof Error ? e.message : String(e));
|
|
@@ -363,23 +441,21 @@ function main() {
|
|
|
363
441
|
void cmdLogout().catch(fatal);
|
|
364
442
|
} else if (cmd === 'whoami') {
|
|
365
443
|
void cmdWhoami().catch(fatal);
|
|
444
|
+
} else if (cmd === 'add') {
|
|
445
|
+
void cmdAdd(firstPositional(rest), parseFlagValue(rest, '--name')).catch(fatal);
|
|
446
|
+
} else if (cmd === 'apps' || cmd === 'ls') {
|
|
447
|
+
void cmdApps().catch(fatal);
|
|
366
448
|
} else if (cmd === 'mcp') {
|
|
367
449
|
void cmdMcp(parseExposed(rest)).catch(fatal);
|
|
368
450
|
} else if (cmd === 'connect') {
|
|
369
451
|
void cmdConnect(parseExposed(rest)).catch(fatal);
|
|
370
452
|
} else if (cmd === 'scan') {
|
|
371
|
-
void cmdScan(rest.find((a) => a.startsWith('http'))).catch(fatal);
|
|
453
|
+
void cmdScan(rest.find((a) => a.startsWith('http')), parseScanProfile(rest)).catch(fatal);
|
|
372
454
|
} else {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
' murk whoami # show login status\n' +
|
|
378
|
-
' murk mcp [--expose http://localhost:3000 ...] # MCP server for your agent; exposes local apps\n' +
|
|
379
|
-
' murk scan http://localhost:3000 # one-shot: connect, scan, disconnect\n' +
|
|
380
|
-
' murk connect --expose http://localhost:3000 # tunnel only (no MCP)\n',
|
|
381
|
-
);
|
|
382
|
-
process.exit(cmd ? 1 : 0);
|
|
455
|
+
const unknown = cmd && cmd !== 'help' && cmd !== '-h' && cmd !== '--help';
|
|
456
|
+
if (unknown) process.stderr.write(`murk: unknown command "${cmd}"\n\n`);
|
|
457
|
+
process.stdout.write(USAGE);
|
|
458
|
+
process.exit(unknown ? 1 : 0);
|
|
383
459
|
}
|
|
384
460
|
}
|
|
385
461
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@murk-security/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Run MURK's MCP server for your AI agent and scan locally-running apps with MURK's hosted runner over a secure outbound tunnel — no local runner, no model key.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"murk.mjs",
|
|
11
|
-
"mcp-server.mjs"
|
|
11
|
+
"mcp-server.mjs",
|
|
12
|
+
"README.md"
|
|
12
13
|
],
|
|
13
14
|
"keywords": [
|
|
14
15
|
"murk",
|