nsauditor-ai 0.1.10 → 0.1.11

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/cli.mjs CHANGED
@@ -13,7 +13,7 @@ import { parseHostArg, parseHostFile } from './utils/host_iterator.mjs';
13
13
  import { buildSarifLog } from './utils/sarif.mjs';
14
14
  import { buildCsv } from './utils/export_csv.mjs';
15
15
  import { recordScan, getLastScan, computeDiff, formatDiffReport, pruneForCE, HISTORY_FILE } from './utils/scan_history.mjs';
16
- import { getTierFromEnv } from './utils/license.mjs';
16
+ import { getTierFromEnv, loadLicense } from './utils/license.mjs';
17
17
  import { resolveCapabilities, hasCapability } from './utils/capabilities.mjs';
18
18
  import { createScheduler } from './utils/scheduler.mjs';
19
19
  import { buildDeltaReport, formatDeltaSummary, hasSignificantChanges } from './utils/delta_reporter.mjs';
@@ -722,23 +722,35 @@ function maxSeverityInConclusion(conclusion) {
722
722
  async function main() {
723
723
  const { cmd, host, plugins, insecureHttps, hostFile, parallel, failOn, outputFormat, watch, intervalMinutes, webhookUrl, alertSeverity, ports } = await parseArgs(process.argv);
724
724
 
725
+ // Verify license JWT at startup (~5ms for ES256). Populates _verifiedTier
726
+ // so all subsequent getTierFromEnv() calls return the cryptographically
727
+ // validated tier instead of relying on prefix detection alone.
728
+ await loadLicense();
729
+
725
730
  if (cmd === 'license') {
726
- const { getTierFromEnv } = await import('./utils/license.mjs');
727
731
  const { resolveCapabilities } = await import('./utils/capabilities.mjs');
728
- // TODO: replace getTierFromEnv() with loadLicense() for full license validation
729
- const tier = getTierFromEnv();
730
- const caps = resolveCapabilities(tier);
731
732
  const key = process.env.NSAUDITOR_LICENSE_KEY;
732
733
  const rawArgs = process.argv.slice(2);
733
734
 
734
735
  if (rawArgs.includes('--status')) {
736
+ const result = await loadLicense(key);
735
737
  const tierLabel = { ce: 'Community Edition (CE)', pro: 'Pro', enterprise: 'Enterprise' };
736
- console.log(`License status: ${tierLabel[tier] ?? tier}`);
737
- console.log(`License key: ${key ? `set (${key.slice(0, 8)}...)` : 'not set — running CE'}`);
738
- if (!key) {
739
- console.log('\n→ Start a free 14-day Pro trial: https://www.nsauditor.com/ai/trial');
738
+ if (result.valid) {
739
+ console.log(`✓ ${tierLabel[result.tier]} license active`);
740
+ console.log(` Org: ${result.org}`);
741
+ console.log(` Seats: ${result.seats}`);
742
+ console.log(` License ID: ${result.licenseId}`);
743
+ console.log(` Expires: ${result.expiresAt}`);
744
+ } else {
745
+ console.log(`✗ ${tierLabel[result.tier] ?? 'Community Edition (CE)'}`);
746
+ console.log(` Reason: ${result.reason}`);
747
+ if (!key) {
748
+ console.log('\n→ Start a free 14-day Pro trial: https://www.nsauditor.com/ai/trial');
749
+ }
740
750
  }
741
751
  } else if (rawArgs.includes('--capabilities')) {
752
+ const tier = getTierFromEnv();
753
+ const caps = resolveCapabilities(tier);
742
754
  console.log(`Active capabilities for tier: ${tier}\n`);
743
755
  for (const [name, enabled] of Object.entries(caps)) {
744
756
  console.log(` ${enabled ? '✓' : '✗'} ${name}`);
package/mcp_server.mjs CHANGED
@@ -19,7 +19,7 @@ import {
19
19
  CallToolRequestSchema,
20
20
  ListToolsRequestSchema,
21
21
  } from '@modelcontextprotocol/sdk/types.js';
22
- import { getTierFromEnv } from './utils/license.mjs';
22
+ import { getTierFromEnv, loadLicense } from './utils/license.mjs';
23
23
  import { resolveCapabilities } from './utils/capabilities.mjs';
24
24
 
25
25
  const _require = createRequire(import.meta.url);
@@ -29,7 +29,8 @@ const { version: TOOL_VERSION } = _require('./package.json');
29
29
  // License tier & capability resolution (module-level, overridable for tests)
30
30
  // ---------------------------------------------------------------------------
31
31
 
32
- // TODO: replace getTierFromEnv() with loadLicense() for full license validation.
32
+ // Module-level: prefix-based tier for immediate use. loadLicense() runs async
33
+ // at server startup (below) and upgrades _tier to the cryptographically verified value.
33
34
  let _tier = getTierFromEnv();
34
35
  let _capabilities = resolveCapabilities(_tier);
35
36
 
@@ -380,6 +381,12 @@ const isMainModule =
380
381
  process.argv[1].endsWith('mcp_server'));
381
382
 
382
383
  if (isMainModule) {
384
+ // Verify license JWT before accepting MCP requests — upgrades _tier from
385
+ // prefix-based to cryptographically verified.
386
+ await loadLicense();
387
+ _tier = getTierFromEnv();
388
+ _capabilities = resolveCapabilities(_tier);
389
+
383
390
  const server = createServer();
384
391
  const transport = new StdioServerTransport();
385
392
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nsauditor-ai",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "Modular AI-assisted network security audit platform — Community Edition",
5
5
  "type": "module",
6
6
  "private": false,
@@ -25,6 +25,7 @@
25
25
  "oui-data": "^1.1.427",
26
26
  "simple-wappalyzer": "^1.1.75",
27
27
  "snmp-native": "^1.2.0",
28
+ "jose": "^6.2.0",
28
29
  "uuid": "^13.0.0",
29
30
  "xml2js": "^0.6.2"
30
31
  },
package/utils/license.mjs CHANGED
@@ -1,27 +1,110 @@
1
1
  // utils/license.mjs
2
- // CE stub — full license validation coming in a future release.
2
+ // JWT license verification for NSAuditor AI.
3
+ // Uses ES256 (ECDSA P-256) public key embedded below — no file I/O needed.
4
+ //
5
+ // KEY ROTATION: If the private key is compromised, generate a new EC P-256 key
6
+ // pair, update PUBLIC_KEY_PEM below, and ship a CE update. All existing JWTs
7
+ // become invalid. See license-manager docs/architecture.md for full procedure.
8
+
9
+ import { jwtVerify, importSPKI } from 'jose';
10
+
11
+ // ES256 public key — embedded directly so it works in npm package (no file read).
12
+ // Corresponding private key is in the license-manager service (NEVER shipped here).
13
+ const PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
14
+ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEDMDuTDV5dPqNafE473AIlCCdbLX7
15
+ u8cSY2dN6mfevYnOydP0SXLHCfWHr+SlpZpA2BiU6GKEk+QdIlWXOgGZsA==
16
+ -----END PUBLIC KEY-----`;
17
+
18
+ // Set by loadLicense(), read by getTierFromEnv().
19
+ // Starts null — before loadLicense() runs, getTierFromEnv() returns 'ce' (safe default).
20
+ // CE (Community Edition) is free and always works without a license.
21
+ let _verifiedTier = null;
3
22
 
4
23
  /**
5
- * Parse tier from NSAUDITOR_LICENSE_KEY environment variable.
6
- * Stub uses key prefix: pro_* → 'pro', enterprise_* 'enterprise'.
24
+ * Synchronous tier detection.
25
+ * Returns 'pro' | 'enterprise' | 'ce'.
26
+ *
27
+ * Before loadLicense() runs: returns 'ce' (Community Edition — safe default).
28
+ * After loadLicense() runs: returns the cryptographically verified tier.
29
+ *
30
+ * CE is the free default — licensed features only activate after loadLicense()
31
+ * confirms the JWT signature. This prevents prefix spoofing from granting
32
+ * elevated privileges during the startup window.
33
+ *
34
+ * MUST remain synchronous — called in hot paths (cli.mjs, plugin_manager, mcp_server).
7
35
  */
8
36
  export function getTierFromEnv() {
9
- const key = process.env.NSAUDITOR_LICENSE_KEY;
10
- if (!key) return 'ce';
11
- if (key.startsWith('pro_')) return 'pro';
12
- if (key.startsWith('enterprise_')) return 'enterprise';
37
+ if (_verifiedTier !== null) return _verifiedTier;
38
+
39
+ // Not yet verified — CE is the safe default.
40
+ // Call loadLicense() at startup to enable Pro/Enterprise.
13
41
  return 'ce';
14
42
  }
15
43
 
16
44
  /**
17
- * Validate a license key string.
18
- * Gracefully degrades to CE on any failure never throws.
45
+ * Full async JWT verification. Call once at startup.
46
+ * On success, caches verified tier so subsequent getTierFromEnv() calls
47
+ * return the cryptographically validated result.
19
48
  *
20
- * @param {string|undefined} keyStr
21
- * @returns {Promise<{valid: boolean, tier: string, reason: string}>}
49
+ * Never throws — degrades to 'ce' on any failure.
50
+ *
51
+ * @param {string} [keyStr] - License key; defaults to NSAUDITOR_LICENSE_KEY env var.
52
+ * @returns {Promise<{valid: boolean, tier: string, org?: string, seats?: number,
53
+ * licenseId?: string, capabilities?: string[], expiresAt?: string, reason?: string}>}
22
54
  */
23
55
  export async function loadLicense(keyStr) {
24
- if (!keyStr) return { valid: false, tier: 'ce', reason: 'no key provided' };
25
- // TODO: implement full license validation
26
- return { valid: false, tier: 'ce', reason: 'license validation not yet implemented' };
56
+ const raw = keyStr ?? process.env.NSAUDITOR_LICENSE_KEY;
57
+ if (!raw) return { valid: false, tier: 'ce', reason: 'no key provided' };
58
+
59
+ // Strip tier prefix
60
+ let token = raw;
61
+ let prefixTier = null;
62
+ if (raw.startsWith('pro_')) { token = raw.slice(4); prefixTier = 'pro'; }
63
+ else if (raw.startsWith('enterprise_')) { token = raw.slice(11); prefixTier = 'enterprise'; }
64
+ else return { valid: false, tier: 'ce', reason: 'unknown key format' };
65
+
66
+ try {
67
+ const publicKey = await importSPKI(PUBLIC_KEY_PEM, 'ES256');
68
+ const { payload } = await jwtVerify(token, publicKey, {
69
+ issuer: 'nsasoft',
70
+ audience: 'nsauditor-ai',
71
+ subject: 'license',
72
+ algorithms: ['ES256'],
73
+ clockTolerance: 120,
74
+ });
75
+
76
+ // Cross-check: prefix must match JWT tier claim
77
+ if (payload.tier !== prefixTier) {
78
+ return { valid: false, tier: 'ce', reason: 'tier mismatch' };
79
+ }
80
+
81
+ // Cache verified tier for synchronous access
82
+ _verifiedTier = payload.tier;
83
+
84
+ return {
85
+ valid: true,
86
+ tier: payload.tier,
87
+ org: payload.org,
88
+ seats: payload.seats,
89
+ licenseId: payload.licenseId,
90
+ capabilities: payload.capabilities,
91
+ expiresAt: new Date(payload.exp * 1000).toISOString(),
92
+ };
93
+ } catch {
94
+ // Verification failure — actively downgrade to CE (prevents prefix spoofing).
95
+ // Generic reason to avoid leaking jose internals to end users.
96
+ _verifiedTier = 'ce';
97
+ return { valid: false, tier: 'ce', reason: 'invalid license key' };
98
+ }
99
+ }
100
+
101
+ /**
102
+ * @internal Test-only. Reset cached verified tier between tests.
103
+ * Disabled in production to prevent accidental tier cache clearing.
104
+ */
105
+ export function _resetCache() {
106
+ if (process.env.NODE_ENV === 'production') {
107
+ throw new Error('_resetCache is test-only and disabled in production');
108
+ }
109
+ _verifiedTier = null;
27
110
  }