nsauditor-ai 0.1.34 → 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 CHANGED
@@ -15,6 +15,68 @@ 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
+
56
+ ## What's New (0.1.35) — CLI provenance footer matches MCP response (so the comparison actually works)
57
+
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.
59
+
60
+ Both CLI commands now emit an identical provenance block:
61
+
62
+ ```
63
+ ── Installation provenance ──
64
+ nsauditor-ai (CE): 0.1.35
65
+ @nsasoft/nsauditor-ai-ee (EE): 0.3.4 (loaded)
66
+ ```
67
+
68
+ **Customer hallucination-detection workflow (5 seconds, no log archeology):**
69
+
70
+ 1. In Claude Desktop: ask "list plugins" → receive a response that should end with the provenance block
71
+ 2. In your terminal: run `nsauditor-ai license --plugins`
72
+ 3. Compare the two `── Installation provenance ──` blocks character-for-character
73
+ 4. **Match** → real MCP `tools/call` happened, response is trustworthy
74
+ 5. **Mismatch / missing block** → Claude fabricated the response (see CE 0.1.33 advisory)
75
+
76
+ This is the v1 mitigation; the v2 (Thread L Phase 2) adds per-call cryptographic sentinel UUIDs that the customer can grep against the server log directly. v1 catches the common case where Claude either omits the block entirely (unlikely to fabricate the new structure verbatim) or includes a stale version pulled from training data.
77
+
78
+ ---
79
+
18
80
  ## What's New (0.1.34) — list_plugins now embeds CE+EE versions for hallucination detection
19
81
 
20
82
  Companion to the 0.1.33 advisory. The `list_plugins` MCP tool's response now appends the actual installed CE + EE version numbers, so customers can verify a Claude Desktop response in **5 seconds** without log archeology:
@@ -379,20 +441,23 @@ nsauditor-ai scan --host 192.168.1.0/24 --plugins all \
379
441
  >
380
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).
381
443
  >
382
- > **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):**
383
445
  >
384
446
  > ```bash
385
- > # Real tier check (ground truth bypasses Claude AI synthesis):
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):
386
454
  > nsauditor-ai mcp tier
387
455
  >
388
- > # Real scan (always hits the network):
456
+ > # Real scan (always hits the network, no MCP client involved):
389
457
  > nsauditor-ai scan --host <X> --plugins all --out <dir>
390
- >
391
- > # Confirm Claude Desktop actually called the MCP server today:
392
- > grep '"method":"tools/call"' ~/Library/Logs/Claude/mcp-server-nsauditor-ai.log | tail -5
393
458
  > ```
394
459
  >
395
- > **SOC 2 evidence + compliance reports MUST be generated via the CLI** — never via the Claude Desktop MCP integration — until this is resolved upstream. Other MCP clients (Claude Code, custom MCP clients via the SDK) appear unaffected. See [What's New (0.1.33)](#whats-new-0133----mcp-integration-with-claude-desktop-is-unreliable) for full details.
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.
396
461
 
397
462
  Expose scanning capabilities to AI assistants via [Model Context Protocol](https://modelcontextprotocol.io):
398
463
 
@@ -543,7 +608,16 @@ claude mcp add nsauditor-ai \
543
608
 
544
609
  > Use the `list_plugins` MCP tool right now and show me the raw tool response verbatim, including the exact text after the JSON.
545
610
 
546
- Then check the MCP log to verify a real call happened:
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):
547
621
 
548
622
  ```bash
549
623
  grep '"method":"tools/call"' ~/Library/Logs/Claude/mcp-server-nsauditor-ai.log | tail -5
package/cli.mjs CHANGED
@@ -5,8 +5,10 @@ import { buildHtmlReport } from './utils/report_html.mjs';
5
5
  import fsp from 'node:fs/promises';
6
6
  import { dirname } from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
+ import { createRequire } from 'node:module';
8
9
 
9
10
  const __dirname = dirname(fileURLToPath(import.meta.url));
11
+ const _require = createRequire(import.meta.url);
10
12
  import path from 'node:path';
11
13
  import { platform } from 'node:os';
12
14
  import { openaiSimplePrompt, openaiPrompt as openaiProPrompt, openaiPromptOptimized } from './utils/prompts.mjs';
@@ -931,6 +933,20 @@ Docs: https://www.nsauditor.com/ai/ | Pricing: https://www.nsauditor.com/ai/
931
933
  console.log('\n→ Start a free 14-day Pro trial: https://www.nsauditor.com/ai/trial');
932
934
  }
933
935
  }
936
+ // CE 0.1.35 (Thread L mitigation v2): version provenance footer
937
+ // matches the MCP server's list_plugins suffix exactly. Customer
938
+ // verification flow: read versions in Claude Desktop's MCP
939
+ // response → compare against `license --status` output here.
940
+ // Mismatch ⇒ Claude hallucinated.
941
+ let _eeVersion = 'not installed';
942
+ try {
943
+ const ee = _require('@nsasoft/nsauditor-ai-ee/package.json');
944
+ _eeVersion = ee && ee.version ? `${ee.version} (loaded)` : 'unknown (loaded)';
945
+ } catch { /* CE-only — fine */ }
946
+ console.log('');
947
+ console.log('── Installation provenance ──');
948
+ console.log(` nsauditor-ai (CE): ${TOOL_VERSION}`);
949
+ console.log(` @nsasoft/nsauditor-ai-ee (EE): ${_eeVersion}`);
934
950
  } else if (rawArgs.includes('--capabilities')) {
935
951
  const tier = getTierFromEnv();
936
952
  const caps = resolveCapabilities(tier);
@@ -1004,6 +1020,24 @@ Docs: https://www.nsauditor.com/ai/ | Pricing: https://www.nsauditor.com/ai/
1004
1020
  console.log('');
1005
1021
  console.log(` ${totalRendered} plugin${totalRendered === 1 ? '' : 's'} total · current tier: ${tier}`);
1006
1022
  }
1023
+
1024
+ // CE 0.1.35 (Thread L mitigation v2): emit installation provenance
1025
+ // identical in shape to the MCP server's list_plugins suffix.
1026
+ // Customers comparing Claude Desktop's MCP response against the
1027
+ // CLI baseline now see the SAME version block in both places.
1028
+ // Mismatch → Claude hallucinated. Match → real tool call.
1029
+ const ceVersion = TOOL_VERSION;
1030
+ let eeVersion = 'not installed';
1031
+ try {
1032
+ const eeManifest = _require('@nsasoft/nsauditor-ai-ee/package.json');
1033
+ eeVersion = eeManifest && eeManifest.version
1034
+ ? `${eeManifest.version} (loaded)`
1035
+ : 'unknown (loaded)';
1036
+ } catch { /* CE-only install — fine */ }
1037
+ console.log('');
1038
+ console.log('── Installation provenance ──');
1039
+ console.log(` nsauditor-ai (CE): ${ceVersion}`);
1040
+ console.log(` @nsasoft/nsauditor-ai-ee (EE): ${eeVersion}`);
1007
1041
  } else if (rawArgs.includes('install')) {
1008
1042
  // CE-0.1.30.4 — install command. Verify the JWT FIRST, then persist
1009
1043
  // to a platform-appropriate location (macOS Keychain / file).
@@ -1505,6 +1539,70 @@ Docs: https://www.nsauditor.com/ai/ | Pricing: https://www.nsauditor.com/ai/
1505
1539
  console.log(` ⚠ ${MCP_AUTH_DISABLE_ENV_VAR}=1 is set — server will start without auth.`);
1506
1540
  }
1507
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
+ }
1508
1606
  } else {
1509
1607
  console.log('Usage:');
1510
1608
  console.log(' nsauditor-ai mcp install-key Generate a new key, persist, print Claude config');
@@ -1513,6 +1611,7 @@ Docs: https://www.nsauditor.com/ai/ | Pricing: https://www.nsauditor.com/ai/
1513
1611
  console.log(' nsauditor-ai mcp rotate-key Replace the stored key with a fresh one');
1514
1612
  console.log(' nsauditor-ai mcp status Show storage source without revealing the key');
1515
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)');
1516
1615
  console.log('');
1517
1616
  console.log('Environment variables:');
1518
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) return 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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nsauditor-ai",
3
- "version": "0.1.34",
3
+ "version": "0.1.36",
4
4
  "description": "Modular AI-assisted network security audit platform — Community Edition",
5
5
  "type": "module",
6
6
  "private": false,