nsauditor-ai 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/README.md +90 -8
- package/bin/nsauditor-ai-mcp.mjs +9 -5
- package/cli.mjs +65 -0
- package/mcp_server.mjs +98 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,6 +15,76 @@ NSAuditor AI is the open-source core of a privacy-first security intelligence pl
|
|
|
15
15
|
|
|
16
16
|
**Zero Data Exfiltration by design.** NSAuditor AI works fully offline. AI analysis, CVE correlation, and continuous monitoring all happen locally. External calls (to AI APIs, NVD, etc.) are opt-in and use your own API keys. We never see your scan data.
|
|
17
17
|
|
|
18
|
+
## What's New (0.1.37) ā š SECURITY FIX: bin shim bypassed auth + license verification
|
|
19
|
+
|
|
20
|
+
**Affects all installations using Claude Desktop (or any MCP client invoking the published `nsauditor-ai-mcp` binary).** Pre-0.1.37, the bin shim at `bin/nsauditor-ai-mcp.mjs` directly called `createServer() + server.connect()` and never invoked the startup block in `mcp_server.mjs` that runs:
|
|
21
|
+
|
|
22
|
+
1. **`authorizeMcpServerStartup()`** ā the `NSA_MCP_AUTH_KEY` enforcement we shipped in EE-SEC.1 (CE 0.1.31). Skipped means **any process with stdio access to the spawned MCP child could call the tools without supplying the auth key**.
|
|
23
|
+
2. **`await loadLicense()`** ā JWT verification of the operator's license key. Skipped means `_tier` stuck at the module-load CE default, so paid Pro/Enterprise customers saw "Current tier: CE" responses and lost MCP access to gated tools entirely.
|
|
24
|
+
3. Rotation cadence warnings, keychain-locked diagnostics ā all silent.
|
|
25
|
+
|
|
26
|
+
**Root cause**: an `argv[1].endsWith('mcp_server.mjs')` guard in `mcp_server.mjs` only matched when the server was invoked directly as `node mcp_server.mjs`. Claude Desktop spawns via the published bin (`nsauditor-ai-mcp`), so the guard was always false in production. The guard existed so that test imports of the module wouldn't auto-start the server ā but the fix should have been to extract the startup into a function the bin shim explicitly calls.
|
|
27
|
+
|
|
28
|
+
**Detection**: in 0.1.36 you could spot this if you noticed your MCP responses said `Current tier: Community Edition (CE)` despite `nsauditor-ai mcp tier` from the shell saying `enterprise`. The disagreement was the 0.1.37 bug surfacing.
|
|
29
|
+
|
|
30
|
+
**Fix in 0.1.37**:
|
|
31
|
+
- Extracted the entire startup sequence into `export async function startStdioServer()` in `mcp_server.mjs`.
|
|
32
|
+
- `bin/nsauditor-ai-mcp.mjs` now imports and awaits `startStdioServer()`. Every Claude Desktop spawn now runs the auth check and license verification it always should have.
|
|
33
|
+
- Regression test (`tests/mcp_bin_startup.test.mjs`) spawns the bin shim with no auth key in env and asserts the auth check refuses startup. If the bin shim ever regresses to bypassing startup again, this test fails.
|
|
34
|
+
|
|
35
|
+
**Action required**: upgrade immediately.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm install -g nsauditor-ai@0.1.37
|
|
39
|
+
# Restart Claude Desktop. Verify with:
|
|
40
|
+
# - Real MCP call from Claude ā response should say "Current tier: Enterprise" (or Pro)
|
|
41
|
+
# - nsauditor-ai mcp verify-call <uuid> ā the 0.1.36 sentinel still works
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**Threat model note**: a process needing stdio access to your Claude Desktop MCP child already had to be running as your user (or able to write to your `~/Library/Application Support/Claude/` config). The auth-bypass exposure is *defense-in-depth degradation*, not "anyone on the internet can call your scanner." But the tier-stuck-at-CE bug definitely cost paying customers actual functionality, and SOC 2 evidence generated from MCP-routed CE-tier responses would fail audit because it lacked enterprise-tier checks.
|
|
45
|
+
|
|
46
|
+
Thanks to the customer who caught this in the wild while we were chasing what looked like a Claude Desktop hallucination ā turned out the bug was on our side.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## What's New (0.1.36) ā cryptographic per-call sentinel UUID (hallucination becomes mathematically detectable)
|
|
51
|
+
|
|
52
|
+
The version-block comparison shipped in 0.1.34/0.1.35 catches lazy hallucinations, but a sufficiently capable AI client can still copy a previously-seen version block from chat context and pass off a fabricated response. **0.1.36 closes that gap** with a per-call cryptographic sentinel that the AI cannot fake.
|
|
53
|
+
|
|
54
|
+
**How it works:**
|
|
55
|
+
- Each `tools/call` invocation mints a fresh server-side UUID via Node's `crypto.randomUUID()`.
|
|
56
|
+
- The UUID is appended to the response text under a `āā Verified MCP call āā` footer.
|
|
57
|
+
- The same UUID is persisted to `~/.nsauditor/mcp-calls.log` (mode 0600, JSON-per-line) **before** the response is returned.
|
|
58
|
+
- A new CLI subcommand `nsauditor-ai mcp verify-call <uuid>` greps the log:
|
|
59
|
+
- **Found** ā the UUID was issued by your local MCP server, so the response bearing it is genuine.
|
|
60
|
+
- **Not found** ā the UUID was never issued, so the entire response was fabricated by the AI client.
|
|
61
|
+
|
|
62
|
+
**Customer verification workflow (10 seconds):**
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# 1. In Claude Desktop, ask Claude to use any MCP tool (e.g., list_plugins).
|
|
66
|
+
# 2. The response ends with:
|
|
67
|
+
# āā Verified MCP call āā
|
|
68
|
+
# call_id: 3f8a1b22-7e44-4c91-9d62-12bd0a4f5e91
|
|
69
|
+
# Verify: nsauditor-ai mcp verify-call 3f8a1b22-7e44-4c91-9d62-12bd0a4f5e91
|
|
70
|
+
# 3. Run that exact verify command in your terminal:
|
|
71
|
+
nsauditor-ai mcp verify-call 3f8a1b22-7e44-4c91-9d62-12bd0a4f5e91
|
|
72
|
+
# ā Verified MCP call ā genuine
|
|
73
|
+
# ā call_id not found ā fabricated (response was AI-generated, not from the MCP server)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
This makes the hallucination detection unfakeable in principle: the AI client has no access to your local Node `crypto.randomUUID()` output, and the sentinel is generated **at the moment the call hits the server** ā there's no way to forge a UUID that will appear in a log file the client cannot read or write.
|
|
77
|
+
|
|
78
|
+
The 0.1.34/0.1.35 version-block check remains as the first line of defense (instant visual mismatch). The 0.1.36 UUID is the cryptographic ground truth for any response you'd act on.
|
|
79
|
+
|
|
80
|
+
`scan_host`, `probe_service`, `get_vulnerabilities`, and `list_plugins` all mint sentinels ā even Pro-tier denials carry a UUID so customers can prove the call reached the server.
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
npm install -g nsauditor-ai@0.1.36
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
18
88
|
## What's New (0.1.35) ā CLI provenance footer matches MCP response (so the comparison actually works)
|
|
19
89
|
|
|
20
90
|
0.1.34 added the version-provenance block to the MCP server's `list_plugins` response, but **the CLI baseline (`license --plugins` / `license --status`) didn't show versions** ā so customers couldn't easily compare. 0.1.35 fixes that asymmetry.
|
|
@@ -403,20 +473,23 @@ nsauditor-ai scan --host 192.168.1.0/24 --plugins all \
|
|
|
403
473
|
>
|
|
404
474
|
> When you use NSAuditor AI through **Claude Desktop's** MCP integration, the AI may **fabricate scan results, plugin lists, vulnerability findings, and tier information without actually invoking the MCP tools**. We've confirmed this empirically: Claude Desktop's permission system shows tool calls being approved, but the actual `tools/call` JSON-RPC messages never reach our server (other MCP servers in the same config receive their calls correctly).
|
|
405
475
|
>
|
|
406
|
-
> **Mandatory verification for any output you'd act on
|
|
476
|
+
> **Mandatory verification for any output you'd act on (NEW in 0.1.36 ā works for any MCP client):**
|
|
407
477
|
>
|
|
408
478
|
> ```bash
|
|
409
|
-
> #
|
|
479
|
+
> # Cryptographic ground truth: copy the call_id from the response footer
|
|
480
|
+
> # ("āā Verified MCP call āā") and run:
|
|
481
|
+
> nsauditor-ai mcp verify-call <call_id>
|
|
482
|
+
> # ā Verified MCP call ā genuine, response is trustworthy
|
|
483
|
+
> # ā call_id not found ā fabricated, IGNORE the response
|
|
484
|
+
>
|
|
485
|
+
> # Real tier check (bypasses Claude AI synthesis):
|
|
410
486
|
> nsauditor-ai mcp tier
|
|
411
487
|
>
|
|
412
|
-
> # Real scan (always hits the network):
|
|
488
|
+
> # Real scan (always hits the network, no MCP client involved):
|
|
413
489
|
> nsauditor-ai scan --host <X> --plugins all --out <dir>
|
|
414
|
-
>
|
|
415
|
-
> # Confirm Claude Desktop actually called the MCP server today:
|
|
416
|
-
> grep '"method":"tools/call"' ~/Library/Logs/Claude/mcp-server-nsauditor-ai.log | tail -5
|
|
417
490
|
> ```
|
|
418
491
|
>
|
|
419
|
-
> **SOC 2 evidence + compliance reports MUST be generated via the CLI** ā never via the Claude Desktop MCP integration ā
|
|
492
|
+
> **SOC 2 evidence + compliance reports MUST be generated via the CLI** ā never via the Claude Desktop MCP integration ā unless every response you act on has a verified call_id. Other MCP clients (Claude Code, custom MCP clients via the SDK) appear unaffected. See [What's New (0.1.36)](#whats-new-0136--cryptographic-per-call-sentinel-uuid-hallucination-becomes-mathematically-detectable) for the cryptographic mitigation and [What's New (0.1.33)](#whats-new-0133----mcp-integration-with-claude-desktop-is-unreliable) for the original advisory.
|
|
420
493
|
|
|
421
494
|
Expose scanning capabilities to AI assistants via [Model Context Protocol](https://modelcontextprotocol.io):
|
|
422
495
|
|
|
@@ -567,7 +640,16 @@ claude mcp add nsauditor-ai \
|
|
|
567
640
|
|
|
568
641
|
> Use the `list_plugins` MCP tool right now and show me the raw tool response verbatim, including the exact text after the JSON.
|
|
569
642
|
|
|
570
|
-
Then
|
|
643
|
+
Then verify a real call happened. The 0.1.36+ way (works for any client, not just Claude Desktop):
|
|
644
|
+
|
|
645
|
+
```bash
|
|
646
|
+
# Copy the call_id from the response footer that Claude returned, then:
|
|
647
|
+
nsauditor-ai mcp verify-call <call_id>
|
|
648
|
+
# ā Verified MCP call ā genuine, response is trustworthy
|
|
649
|
+
# ā call_id not found ā fabricated, IGNORE the response
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
The pre-0.1.36 fallback (Claude Desktop log archeology):
|
|
571
653
|
|
|
572
654
|
```bash
|
|
573
655
|
grep '"method":"tools/call"' ~/Library/Logs/Claude/mcp-server-nsauditor-ai.log | tail -5
|
package/bin/nsauditor-ai-mcp.mjs
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
// CE 0.1.37 (SECURITY): delegate to startStdioServer() so the auth
|
|
3
|
+
// check + license verification + rotation warnings actually run.
|
|
4
|
+
// Pre-0.1.37 this file inlined `createServer() + server.connect()`,
|
|
5
|
+
// which silently bypassed the entire startup block in mcp_server.mjs
|
|
6
|
+
// (it was guarded by an argv[1].endsWith('mcp_server.mjs') check that
|
|
7
|
+
// only matched when invoked directly, never via this shim). Result:
|
|
8
|
+
// every Claude Desktop session ran unauthenticated and tier-stuck-at-CE.
|
|
9
|
+
import { startStdioServer } from '../mcp_server.mjs';
|
|
4
10
|
|
|
5
|
-
|
|
6
|
-
const transport = new StdioServerTransport();
|
|
7
|
-
await server.connect(transport);
|
|
11
|
+
await startStdioServer();
|
package/cli.mjs
CHANGED
|
@@ -1539,6 +1539,70 @@ Docs: https://www.nsauditor.com/ai/ | Pricing: https://www.nsauditor.com/ai/
|
|
|
1539
1539
|
console.log(` ā ${MCP_AUTH_DISABLE_ENV_VAR}=1 is set ā server will start without auth.`);
|
|
1540
1540
|
}
|
|
1541
1541
|
}
|
|
1542
|
+
} else if (subCmd === 'verify-call') {
|
|
1543
|
+
// CE 0.1.36 (Thread L Phase 2): cryptographic ground truth for
|
|
1544
|
+
// "did Claude actually call the MCP server, or hallucinate a
|
|
1545
|
+
// response?" Server mints a fresh UUID per tools/call, embeds it
|
|
1546
|
+
// in the response text, AND appends to ~/.nsauditor/mcp-calls.log.
|
|
1547
|
+
// Customer pastes the UUID here; we grep the log. UUID present ā
|
|
1548
|
+
// proven real call. UUID absent ā fabricated (or log was rotated/
|
|
1549
|
+
// deleted; we say "unverifiable" rather than "fake").
|
|
1550
|
+
const { readFile, stat } = await import('node:fs/promises');
|
|
1551
|
+
const { join: _join } = await import('node:path');
|
|
1552
|
+
const { homedir: _homedir } = await import('node:os');
|
|
1553
|
+
const logPath = _join(_homedir(), '.nsauditor', 'mcp-calls.log');
|
|
1554
|
+
const uuid = rawArgs[2];
|
|
1555
|
+
if (!uuid) {
|
|
1556
|
+
console.error('Usage: nsauditor-ai mcp verify-call <uuid>');
|
|
1557
|
+
console.error(' Paste the call_id from the MCP tool response footer.');
|
|
1558
|
+
process.exit(2);
|
|
1559
|
+
}
|
|
1560
|
+
// Conservative UUID v4 shape check ā avoid grepping the log with
|
|
1561
|
+
// arbitrary user input.
|
|
1562
|
+
if (!/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(uuid)) {
|
|
1563
|
+
console.error(`ā Not a valid UUID: ${uuid}`);
|
|
1564
|
+
console.error(' Expected format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx');
|
|
1565
|
+
process.exit(2);
|
|
1566
|
+
}
|
|
1567
|
+
let logExists = false;
|
|
1568
|
+
try { await stat(logPath); logExists = true; } catch { /* missing */ }
|
|
1569
|
+
if (!logExists) {
|
|
1570
|
+
console.log(`ā No MCP call log at ${logPath}`);
|
|
1571
|
+
console.log(' Either: (a) MCP server has never been invoked from this account, or');
|
|
1572
|
+
console.log(' (b) the log was deleted/rotated.');
|
|
1573
|
+
console.log(' Trigger one real call (e.g., ask Claude "use list_plugins") then retry.');
|
|
1574
|
+
process.exit(1);
|
|
1575
|
+
}
|
|
1576
|
+
const raw = await readFile(logPath, 'utf8');
|
|
1577
|
+
// Look for an exact JSON-string match on call_id to avoid prefix collisions.
|
|
1578
|
+
const needle = `"call_id":"${uuid.toLowerCase()}"`;
|
|
1579
|
+
const lines = raw.split('\n').filter((l) => l.includes(needle));
|
|
1580
|
+
if (lines.length === 0) {
|
|
1581
|
+
console.log(`ā call_id not found in ${logPath}`);
|
|
1582
|
+
console.log('');
|
|
1583
|
+
console.log(` ${uuid}`);
|
|
1584
|
+
console.log('');
|
|
1585
|
+
console.log(' This UUID was NOT issued by this MCP server. Most likely cause:');
|
|
1586
|
+
console.log(' Claude Desktop fabricated the response without invoking the server.');
|
|
1587
|
+
console.log(' (See README §"Verifying that Claude actually called the MCP server".)');
|
|
1588
|
+
process.exit(1);
|
|
1589
|
+
}
|
|
1590
|
+
try {
|
|
1591
|
+
const entry = JSON.parse(lines[lines.length - 1]);
|
|
1592
|
+
console.log(`ā Verified MCP call`);
|
|
1593
|
+
console.log(` call_id: ${entry.call_id}`);
|
|
1594
|
+
console.log(` tool: ${entry.tool}`);
|
|
1595
|
+
console.log(` ts: ${entry.ts}`);
|
|
1596
|
+
console.log(` log: ${logPath}`);
|
|
1597
|
+
console.log('');
|
|
1598
|
+
console.log(' This UUID was issued by the local MCP server, so the response');
|
|
1599
|
+
console.log(' bearing it was a genuine tool call (not a hallucination).');
|
|
1600
|
+
process.exit(0);
|
|
1601
|
+
} catch {
|
|
1602
|
+
console.log(`ā Verified MCP call (matched ${lines.length} log line(s) for this UUID)`);
|
|
1603
|
+
console.log(` log: ${logPath}`);
|
|
1604
|
+
process.exit(0);
|
|
1605
|
+
}
|
|
1542
1606
|
} else {
|
|
1543
1607
|
console.log('Usage:');
|
|
1544
1608
|
console.log(' nsauditor-ai mcp install-key Generate a new key, persist, print Claude config');
|
|
@@ -1547,6 +1611,7 @@ Docs: https://www.nsauditor.com/ai/ | Pricing: https://www.nsauditor.com/ai/
|
|
|
1547
1611
|
console.log(' nsauditor-ai mcp rotate-key Replace the stored key with a fresh one');
|
|
1548
1612
|
console.log(' nsauditor-ai mcp status Show storage source without revealing the key');
|
|
1549
1613
|
console.log(' nsauditor-ai mcp tier Print actual MCP server tier (ground truth, bypasses Claude AI synthesis)');
|
|
1614
|
+
console.log(' nsauditor-ai mcp verify-call <uuid> Prove a tool response came from the real MCP server (not Claude hallucination)');
|
|
1550
1615
|
console.log('');
|
|
1551
1616
|
console.log('Environment variables:');
|
|
1552
1617
|
console.log(` ${MCP_AUTH_ENV_VAR} Read by mcp_server.mjs at startup; client supplies via Claude config`);
|
package/mcp_server.mjs
CHANGED
|
@@ -8,8 +8,11 @@
|
|
|
8
8
|
// import { createServer, toolHandlers } from './mcp_server.mjs' ā for testing
|
|
9
9
|
|
|
10
10
|
import { createRequire } from 'node:module';
|
|
11
|
-
import { dirname } from 'node:path';
|
|
11
|
+
import { dirname, join } from 'node:path';
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
import { randomUUID } from 'node:crypto';
|
|
15
|
+
import { appendFile, mkdir, chmod } from 'node:fs/promises';
|
|
13
16
|
|
|
14
17
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
18
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
@@ -99,6 +102,56 @@ export function _setValidateHost(fn) {
|
|
|
99
102
|
_validateHostFn = fn ?? validateHost;
|
|
100
103
|
}
|
|
101
104
|
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// Per-call cryptographic sentinel (CE 0.1.36 ā Thread L Phase 2)
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
//
|
|
109
|
+
// Why this exists: even after CE 0.1.34 embedded the resolved tier and CE 0.1.35
|
|
110
|
+
// added a CLI provenance footer, Claude Desktop was empirically observed
|
|
111
|
+
// (2026-05-09, kankanyan@gmail.com) fabricating list_plugins responses
|
|
112
|
+
// WITHOUT routing to this server (per-server log: 0 tools/call entries
|
|
113
|
+
// while other configured MCP servers received 50+ in the same session).
|
|
114
|
+
// A fabricated response can copy any text it has seen ā including version
|
|
115
|
+
// numbers from the CLI footer. The only thing it cannot copy is a
|
|
116
|
+
// random UUID generated server-side AT THE MOMENT of the call.
|
|
117
|
+
//
|
|
118
|
+
// Each tool invocation gets a fresh UUID. The UUID is:
|
|
119
|
+
// 1. Embedded in the response text (Claude cannot omit; it's the payload)
|
|
120
|
+
// 2. Persisted to ~/.nsauditor/mcp-calls.log (append-only, mode 0600)
|
|
121
|
+
//
|
|
122
|
+
// Customer verification: paste the UUID from Claude into
|
|
123
|
+
// nsauditor-ai mcp verify-call <uuid>
|
|
124
|
+
// If it appears in the local log ā real call. If not ā hallucinated.
|
|
125
|
+
// (See cli.mjs `verify-call` subcommand.)
|
|
126
|
+
const MCP_CALL_LOG_PATH = join(homedir(), '.nsauditor', 'mcp-calls.log');
|
|
127
|
+
|
|
128
|
+
async function recordToolCall(toolName) {
|
|
129
|
+
const callId = randomUUID();
|
|
130
|
+
const ts = new Date().toISOString();
|
|
131
|
+
// Best-effort: a log-write failure must not break the customer's tool
|
|
132
|
+
// call. Verification will simply fail-closed (UUID-not-found ā treat as
|
|
133
|
+
// unverifiable rather than as proof-of-fake).
|
|
134
|
+
try {
|
|
135
|
+
await mkdir(dirname(MCP_CALL_LOG_PATH), { recursive: true });
|
|
136
|
+
const line = JSON.stringify({ call_id: callId, tool: toolName, ts }) + '\n';
|
|
137
|
+
await appendFile(MCP_CALL_LOG_PATH, line, { encoding: 'utf8' });
|
|
138
|
+
// Tighten on first write; chmod is idempotent so cheap to repeat.
|
|
139
|
+
try { await chmod(MCP_CALL_LOG_PATH, 0o600); } catch { /* non-fatal on Windows */ }
|
|
140
|
+
} catch (err) {
|
|
141
|
+
process.stderr.write(`[nsauditor-mcp] call-log write failed: ${err.message}\n`);
|
|
142
|
+
}
|
|
143
|
+
return callId;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function appendCallSentinel(text, callId) {
|
|
147
|
+
return (
|
|
148
|
+
`${text}\n\nāā Verified MCP call āā\n` +
|
|
149
|
+
`call_id: ${callId}\n` +
|
|
150
|
+
`Verify (proves Claude actually called this server, not hallucinated):\n` +
|
|
151
|
+
` nsauditor-ai mcp verify-call ${callId}`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
102
155
|
// ---------------------------------------------------------------------------
|
|
103
156
|
// Tool definitions (JSON Schema for input validation)
|
|
104
157
|
// ---------------------------------------------------------------------------
|
|
@@ -357,10 +410,23 @@ export function createServer() {
|
|
|
357
410
|
};
|
|
358
411
|
}
|
|
359
412
|
|
|
413
|
+
// CE 0.1.36 (Thread L Phase 2): mint a per-call sentinel UUID and
|
|
414
|
+
// log it BEFORE the Pro-gate so even denials prove the call hit
|
|
415
|
+
// the server. Fabricated responses cannot include a UUID that
|
|
416
|
+
// exists in the customer's local log file.
|
|
417
|
+
const callId = await recordToolCall(name);
|
|
418
|
+
|
|
360
419
|
// Gate Pro-tier tools at the MCP dispatch layer
|
|
361
420
|
if (name === 'probe_service' || name === 'get_vulnerabilities') {
|
|
362
421
|
const denied = requireProCapability(name);
|
|
363
|
-
if (denied)
|
|
422
|
+
if (denied) {
|
|
423
|
+
return {
|
|
424
|
+
...denied,
|
|
425
|
+
content: denied.content.map((c) =>
|
|
426
|
+
c.type === 'text' ? { ...c, text: appendCallSentinel(c.text, callId) } : c,
|
|
427
|
+
),
|
|
428
|
+
};
|
|
429
|
+
}
|
|
364
430
|
}
|
|
365
431
|
|
|
366
432
|
try {
|
|
@@ -408,16 +474,16 @@ export function createServer() {
|
|
|
408
474
|
const tierSuffix = `\n\nCurrent tier: ${tierLabel[_tier] ?? _tier}. ${_capabilities.proMCP ? '' : 'Upgrade to Pro for probe_service, get_vulnerabilities, risk_summary, and more.'}`;
|
|
409
475
|
|
|
410
476
|
return {
|
|
411
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) + tierSuffix + versionLines }],
|
|
477
|
+
content: [{ type: 'text', text: appendCallSentinel(JSON.stringify(result, null, 2) + tierSuffix + versionLines, callId) }],
|
|
412
478
|
};
|
|
413
479
|
}
|
|
414
480
|
|
|
415
481
|
return {
|
|
416
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
482
|
+
content: [{ type: 'text', text: appendCallSentinel(JSON.stringify(result, null, 2), callId) }],
|
|
417
483
|
};
|
|
418
484
|
} catch (err) {
|
|
419
485
|
return {
|
|
420
|
-
content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }],
|
|
486
|
+
content: [{ type: 'text', text: appendCallSentinel(JSON.stringify({ error: err.message }), callId) }],
|
|
421
487
|
isError: true,
|
|
422
488
|
};
|
|
423
489
|
}
|
|
@@ -427,16 +493,22 @@ export function createServer() {
|
|
|
427
493
|
}
|
|
428
494
|
|
|
429
495
|
// ---------------------------------------------------------------------------
|
|
430
|
-
//
|
|
496
|
+
// Stdio entry point ā used by bin/nsauditor-ai-mcp.mjs AND `node mcp_server.mjs`
|
|
431
497
|
// ---------------------------------------------------------------------------
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
498
|
+
//
|
|
499
|
+
// CE 0.1.37 (SECURITY): this used to be guarded by a brittle
|
|
500
|
+
// `process.argv[1].endsWith('mcp_server.mjs')` check. The bin shim
|
|
501
|
+
// (which Claude Desktop spawns) sets argv[1] to `nsauditor-ai-mcp.mjs`,
|
|
502
|
+
// so the guard was false and the entire startup block ā auth check,
|
|
503
|
+
// license verification, rotation warnings ā was SKIPPED. Result: Claude
|
|
504
|
+
// Desktop's MCP child ran unauthenticated, with _tier stuck at the CE
|
|
505
|
+
// default, regardless of the operator's installed license. Customers
|
|
506
|
+
// paying for Pro/Enterprise saw "Current tier: CE" responses and lost
|
|
507
|
+
// MCP access to gated tools entirely.
|
|
508
|
+
//
|
|
509
|
+
// Fix: extract the startup into an exported function. The bin shim now
|
|
510
|
+
// calls it explicitly, so the auth + license path runs every time.
|
|
511
|
+
export async function startStdioServer() {
|
|
440
512
|
// EE-SEC.1: enforce MCP server authentication BEFORE accepting any
|
|
441
513
|
// tool calls. Pre-fold any process running as the operator could
|
|
442
514
|
// spawn the server and call Pro/Enterprise tools ā including the
|
|
@@ -523,4 +595,16 @@ if (isMainModule) {
|
|
|
523
595
|
const server = createServer();
|
|
524
596
|
const transport = new StdioServerTransport();
|
|
525
597
|
await server.connect(transport);
|
|
598
|
+
return server;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// Backward-compat: still callable as `node mcp_server.mjs` directly.
|
|
602
|
+
const isMainModule =
|
|
603
|
+
typeof process !== 'undefined' &&
|
|
604
|
+
process.argv[1] &&
|
|
605
|
+
(process.argv[1].endsWith('mcp_server.mjs') ||
|
|
606
|
+
process.argv[1].endsWith('mcp_server'));
|
|
607
|
+
|
|
608
|
+
if (isMainModule) {
|
|
609
|
+
await startStdioServer();
|
|
526
610
|
}
|