nsauditor-ai 0.1.35 → 0.1.36
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 +58 -8
- package/cli.mjs +65 -0
- package/mcp_server.mjs +71 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,6 +15,44 @@ 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.36) — cryptographic per-call sentinel UUID (hallucination becomes mathematically detectable)
|
|
19
|
+
|
|
20
|
+
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.
|
|
21
|
+
|
|
22
|
+
**How it works:**
|
|
23
|
+
- Each `tools/call` invocation mints a fresh server-side UUID via Node's `crypto.randomUUID()`.
|
|
24
|
+
- The UUID is appended to the response text under a `── Verified MCP call ──` footer.
|
|
25
|
+
- The same UUID is persisted to `~/.nsauditor/mcp-calls.log` (mode 0600, JSON-per-line) **before** the response is returned.
|
|
26
|
+
- A new CLI subcommand `nsauditor-ai mcp verify-call <uuid>` greps the log:
|
|
27
|
+
- **Found** → the UUID was issued by your local MCP server, so the response bearing it is genuine.
|
|
28
|
+
- **Not found** → the UUID was never issued, so the entire response was fabricated by the AI client.
|
|
29
|
+
|
|
30
|
+
**Customer verification workflow (10 seconds):**
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# 1. In Claude Desktop, ask Claude to use any MCP tool (e.g., list_plugins).
|
|
34
|
+
# 2. The response ends with:
|
|
35
|
+
# ── Verified MCP call ──
|
|
36
|
+
# call_id: 3f8a1b22-7e44-4c91-9d62-12bd0a4f5e91
|
|
37
|
+
# Verify: nsauditor-ai mcp verify-call 3f8a1b22-7e44-4c91-9d62-12bd0a4f5e91
|
|
38
|
+
# 3. Run that exact verify command in your terminal:
|
|
39
|
+
nsauditor-ai mcp verify-call 3f8a1b22-7e44-4c91-9d62-12bd0a4f5e91
|
|
40
|
+
# ✓ Verified MCP call → genuine
|
|
41
|
+
# ✗ call_id not found → fabricated (response was AI-generated, not from the MCP server)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
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.
|
|
45
|
+
|
|
46
|
+
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.
|
|
47
|
+
|
|
48
|
+
`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.
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm install -g nsauditor-ai@0.1.36
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
18
56
|
## What's New (0.1.35) — CLI provenance footer matches MCP response (so the comparison actually works)
|
|
19
57
|
|
|
20
58
|
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 +441,23 @@ nsauditor-ai scan --host 192.168.1.0/24 --plugins all \
|
|
|
403
441
|
>
|
|
404
442
|
> 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
443
|
>
|
|
406
|
-
> **Mandatory verification for any output you'd act on
|
|
444
|
+
> **Mandatory verification for any output you'd act on (NEW in 0.1.36 — works for any MCP client):**
|
|
407
445
|
>
|
|
408
446
|
> ```bash
|
|
409
|
-
> #
|
|
447
|
+
> # Cryptographic ground truth: copy the call_id from the response footer
|
|
448
|
+
> # ("── Verified MCP call ──") and run:
|
|
449
|
+
> nsauditor-ai mcp verify-call <call_id>
|
|
450
|
+
> # ✓ Verified MCP call → genuine, response is trustworthy
|
|
451
|
+
> # ✗ call_id not found → fabricated, IGNORE the response
|
|
452
|
+
>
|
|
453
|
+
> # Real tier check (bypasses Claude AI synthesis):
|
|
410
454
|
> nsauditor-ai mcp tier
|
|
411
455
|
>
|
|
412
|
-
> # Real scan (always hits the network):
|
|
456
|
+
> # Real scan (always hits the network, no MCP client involved):
|
|
413
457
|
> 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
458
|
> ```
|
|
418
459
|
>
|
|
419
|
-
> **SOC 2 evidence + compliance reports MUST be generated via the CLI** — never via the Claude Desktop MCP integration —
|
|
460
|
+
> **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
461
|
|
|
421
462
|
Expose scanning capabilities to AI assistants via [Model Context Protocol](https://modelcontextprotocol.io):
|
|
422
463
|
|
|
@@ -567,7 +608,16 @@ claude mcp add nsauditor-ai \
|
|
|
567
608
|
|
|
568
609
|
> Use the `list_plugins` MCP tool right now and show me the raw tool response verbatim, including the exact text after the JSON.
|
|
569
610
|
|
|
570
|
-
Then
|
|
611
|
+
Then verify a real call happened. The 0.1.36+ way (works for any client, not just Claude Desktop):
|
|
612
|
+
|
|
613
|
+
```bash
|
|
614
|
+
# Copy the call_id from the response footer that Claude returned, then:
|
|
615
|
+
nsauditor-ai mcp verify-call <call_id>
|
|
616
|
+
# ✓ Verified MCP call → genuine, response is trustworthy
|
|
617
|
+
# ✗ call_id not found → fabricated, IGNORE the response
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
The pre-0.1.36 fallback (Claude Desktop log archeology):
|
|
571
621
|
|
|
572
622
|
```bash
|
|
573
623
|
grep '"method":"tools/call"' ~/Library/Logs/Claude/mcp-server-nsauditor-ai.log | tail -5
|
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
|
}
|