nsauditor-ai 0.1.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.
Files changed (60) hide show
  1. package/CONTRIBUTING.md +24 -0
  2. package/LICENSE +21 -0
  3. package/README.md +584 -0
  4. package/bin/nsauditor-ai-mcp.mjs +2 -0
  5. package/bin/nsauditor-ai.mjs +2 -0
  6. package/cli.mjs +939 -0
  7. package/config/services.json +304 -0
  8. package/docs/EULA-nsauditor-ai.md +324 -0
  9. package/index.mjs +15 -0
  10. package/mcp_server.mjs +382 -0
  11. package/package.json +44 -0
  12. package/plugin_manager.mjs +829 -0
  13. package/plugins/arp_scanner.mjs +162 -0
  14. package/plugins/db_scanner.mjs +248 -0
  15. package/plugins/dns_scanner.mjs +369 -0
  16. package/plugins/dnssd-scanner.mjs +245 -0
  17. package/plugins/ftp_banner_check.mjs +247 -0
  18. package/plugins/host_up_check.mjs +337 -0
  19. package/plugins/http_probe.mjs +290 -0
  20. package/plugins/llmnr_scanner.mjs +130 -0
  21. package/plugins/mdns_scanner.mjs +522 -0
  22. package/plugins/netbios_scanner.mjs +737 -0
  23. package/plugins/opensearch_scanner.mjs +276 -0
  24. package/plugins/os_detector.mjs +436 -0
  25. package/plugins/ping_checker.mjs +271 -0
  26. package/plugins/port_scanner.mjs +250 -0
  27. package/plugins/result_concluder.mjs +274 -0
  28. package/plugins/snmp_scanner.mjs +278 -0
  29. package/plugins/ssh_scanner.mjs +421 -0
  30. package/plugins/sunrpc_scanner.mjs +339 -0
  31. package/plugins/syn_scanner.mjs +314 -0
  32. package/plugins/tls_scanner.mjs +225 -0
  33. package/plugins/upnp_scanner.mjs +441 -0
  34. package/plugins/webapp_detector.mjs +246 -0
  35. package/plugins/wsd_scanner.mjs +290 -0
  36. package/utils/attack_map.mjs +180 -0
  37. package/utils/capabilities.mjs +53 -0
  38. package/utils/conclusion_utils.mjs +70 -0
  39. package/utils/cpe.mjs +74 -0
  40. package/utils/cve_validator.mjs +64 -0
  41. package/utils/cvss.mjs +129 -0
  42. package/utils/delta_reporter.mjs +110 -0
  43. package/utils/export_csv.mjs +82 -0
  44. package/utils/finding_queue.mjs +64 -0
  45. package/utils/finding_schema.mjs +36 -0
  46. package/utils/host_iterator.mjs +166 -0
  47. package/utils/license.mjs +29 -0
  48. package/utils/net_validation.mjs +66 -0
  49. package/utils/nvd_cache.mjs +77 -0
  50. package/utils/nvd_client.mjs +130 -0
  51. package/utils/oui.mjs +107 -0
  52. package/utils/plugin_discovery.mjs +89 -0
  53. package/utils/prompts.mjs +143 -0
  54. package/utils/raw_report_html.mjs +170 -0
  55. package/utils/redact.mjs +79 -0
  56. package/utils/report_html.mjs +236 -0
  57. package/utils/sarif.mjs +225 -0
  58. package/utils/scan_history.mjs +248 -0
  59. package/utils/scheduler.mjs +157 -0
  60. package/utils/webhook.mjs +177 -0
package/mcp_server.mjs ADDED
@@ -0,0 +1,382 @@
1
+ #!/usr/bin/env node
2
+ // mcp_server.mjs
3
+ // MCP (Model Context Protocol) server for nsauditor plugin manager.
4
+ // Exposes scan, probe, vulnerability lookup, and plugin listing tools.
5
+ //
6
+ // Usage:
7
+ // node mcp_server.mjs — starts stdio transport
8
+ // import { createServer, toolHandlers } from './mcp_server.mjs' — for testing
9
+
10
+ import { createRequire } from 'node:module';
11
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
12
+ import { resolveAndValidate } from './utils/net_validation.mjs';
13
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
14
+ import {
15
+ CallToolRequestSchema,
16
+ ListToolsRequestSchema,
17
+ } from '@modelcontextprotocol/sdk/types.js';
18
+ import { getTierFromEnv } from './utils/license.mjs';
19
+ import { resolveCapabilities } from './utils/capabilities.mjs';
20
+
21
+ const _require = createRequire(import.meta.url);
22
+ const { version: TOOL_VERSION } = _require('./package.json');
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // License tier & capability resolution (module-level, overridable for tests)
26
+ // ---------------------------------------------------------------------------
27
+
28
+ // TODO (Phase 2): replace getTierFromEnv() with loadLicense(process.env.NSAUDITOR_LICENSE_KEY)
29
+ // and wire the returned tier here. Until then, pro_* prefix grants Pro tier without verification.
30
+ let _tier = getTierFromEnv();
31
+ let _capabilities = resolveCapabilities(_tier);
32
+
33
+ /**
34
+ * @internal Test-only. Override tier without touching env vars.
35
+ * Do NOT use in production code. When JWT license validation lands (Phase 2),
36
+ * this function will be removed or guarded by NODE_ENV !== 'production'.
37
+ */
38
+ export function _setTier(tier) {
39
+ if (process.env.NODE_ENV === 'production') throw new Error('_setTier is test-only and disabled in production');
40
+ _tier = tier ?? getTierFromEnv();
41
+ _capabilities = resolveCapabilities(_tier);
42
+ }
43
+
44
+ /** @internal Test-only. Returns the Pro-gate denial object (or null if allowed). */
45
+ export function _requireProCapability(toolName) {
46
+ if (process.env.NODE_ENV === 'production') throw new Error('_requireProCapability is test-only and disabled in production');
47
+ return requireProCapability(toolName);
48
+ }
49
+
50
+ function requireProCapability(toolName) {
51
+ if (_capabilities.proMCP) return null; // Pro/Enterprise: allow
52
+ return {
53
+ content: [{
54
+ type: 'text',
55
+ text: `🔒 **${toolName}** requires a Pro license.\n\nUpgrade at https://www.nsauditor.com/ai/pricing or start a free 14-day trial (no credit card) at https://www.nsauditor.com/ai/trial\n\n**CE tools available:** scan_host, list_plugins`,
56
+ }],
57
+ isError: true,
58
+ };
59
+ }
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Lazy singletons — initialised on first use, overridable for tests
63
+ // ---------------------------------------------------------------------------
64
+
65
+ let _pluginManager = null;
66
+ let _nvdClient = null;
67
+
68
+ async function getPluginManager() {
69
+ if (_pluginManager) return _pluginManager;
70
+ const { default: PluginManager } = await import('./plugin_manager.mjs');
71
+ _pluginManager = await PluginManager.create('./plugins');
72
+ return _pluginManager;
73
+ }
74
+
75
+ async function getNvdClient() {
76
+ if (_nvdClient) return _nvdClient;
77
+ const { createNvdClient } = await import('./utils/nvd_client.mjs');
78
+ _nvdClient = createNvdClient();
79
+ return _nvdClient;
80
+ }
81
+
82
+ /** Allow tests to inject mocks without touching the real modules. */
83
+ export function _setPluginManager(pm) {
84
+ if (process.env.NODE_ENV === 'production') throw new Error('_setPluginManager is test-only and disabled in production');
85
+ _pluginManager = pm;
86
+ }
87
+ export function _setNvdClient(client) {
88
+ if (process.env.NODE_ENV === 'production') throw new Error('_setNvdClient is test-only and disabled in production');
89
+ _nvdClient = client;
90
+ }
91
+
92
+ let _validateHostFn = validateHost;
93
+ export function _setValidateHost(fn) {
94
+ if (process.env.NODE_ENV === 'production') throw new Error('_setValidateHost is test-only and disabled in production');
95
+ _validateHostFn = fn ?? validateHost;
96
+ }
97
+
98
+ // ---------------------------------------------------------------------------
99
+ // Tool definitions (JSON Schema for input validation)
100
+ // ---------------------------------------------------------------------------
101
+
102
+ const TOOLS = [
103
+ {
104
+ name: 'scan_host',
105
+ description:
106
+ 'Run a full plugin scan on a target host and return structured results including service detection, OS fingerprinting, and security findings.',
107
+ inputSchema: {
108
+ type: 'object',
109
+ properties: {
110
+ host: {
111
+ type: 'string',
112
+ description: 'Target hostname or IP address to scan',
113
+ },
114
+ timeout: {
115
+ type: 'number',
116
+ description: 'Per-plugin timeout in milliseconds (default: 30000)',
117
+ },
118
+ },
119
+ required: ['host'],
120
+ },
121
+ },
122
+ {
123
+ name: 'probe_service',
124
+ description:
125
+ 'Run a specific plugin against a host:port combination to probe a single service.',
126
+ inputSchema: {
127
+ type: 'object',
128
+ properties: {
129
+ host: {
130
+ type: 'string',
131
+ description: 'Target hostname or IP address',
132
+ },
133
+ port: {
134
+ type: 'number',
135
+ description: 'Target port number',
136
+ },
137
+ pluginName: {
138
+ type: 'string',
139
+ description: 'Plugin name or ID to run (e.g. "ssh_scanner" or "002")',
140
+ },
141
+ },
142
+ required: ['host', 'port', 'pluginName'],
143
+ },
144
+ },
145
+ {
146
+ name: 'get_vulnerabilities',
147
+ description:
148
+ 'Look up known CVEs for a given CPE (Common Platform Enumeration) string using the NVD API.',
149
+ inputSchema: {
150
+ type: 'object',
151
+ properties: {
152
+ cpe: {
153
+ type: 'string',
154
+ description:
155
+ 'CPE 2.3 string, e.g. "cpe:2.3:a:apache:log4j:2.14.1:*:*:*:*:*:*:*"',
156
+ },
157
+ maxResults: {
158
+ type: 'number',
159
+ description: 'Maximum number of CVE results to return (default: all)',
160
+ },
161
+ },
162
+ required: ['cpe'],
163
+ },
164
+ },
165
+ {
166
+ name: 'list_plugins',
167
+ description:
168
+ 'Return the list of available audit plugins with their IDs, names, priorities, and requirements.',
169
+ inputSchema: {
170
+ type: 'object',
171
+ properties: {},
172
+ required: [],
173
+ },
174
+ },
175
+ ];
176
+
177
+ // ---------------------------------------------------------------------------
178
+ // Input validation (trust boundary — MCP clients are external)
179
+ // ---------------------------------------------------------------------------
180
+
181
+ /**
182
+ * Validate host to prevent SSRF via loopback, link-local, or cloud metadata.
183
+ * Performs DNS resolution to defeat rebinding / encoded-IP bypasses.
184
+ * @param {string} host
185
+ * @returns {Promise<string>} normalised hostname
186
+ */
187
+ export async function validateHost(host) {
188
+ const h = String(host).trim().toLowerCase();
189
+ if (!h) throw new Error('Empty host');
190
+ // Fast-path: reject decimal-encoded loopback IPs (127.0.0.0/8).
191
+ // Other private/link-local ranges (RFC 1918, 169.254.x.x) are caught by the regex below
192
+ // and by the DNS resolveAndValidate() layer for all encoding forms.
193
+ const _isAllDigits = /^\d+$/.test(h);
194
+ const _n = _isAllDigits && h.length <= 10 ? Number(h) : -1;
195
+ const isDecimalLoopback = _n >= 0x7F000000 && _n <= 0x7FFFFFFF;
196
+ if (isDecimalLoopback || /^(localhost|127\.|0\.|::1|0\.0\.0\.0|169\.254\.|fe80:|metadata\.google)/i.test(h)) {
197
+ throw new Error('Scanning loopback, link-local, or metadata addresses is not allowed via MCP');
198
+ }
199
+
200
+ // DNS resolution check — catches rebinding, decimal/octal IPs, IPv6-mapped addrs
201
+ try {
202
+ await resolveAndValidate(h);
203
+ } catch (err) {
204
+ throw new Error('Scanning loopback, link-local, or metadata addresses is not allowed via MCP');
205
+ }
206
+ return h;
207
+ }
208
+
209
+ /** Validate port is an integer in 1-65535 range. */
210
+ function validatePort(port) {
211
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
212
+ throw new Error('port must be an integer between 1 and 65535');
213
+ }
214
+ return port;
215
+ }
216
+
217
+ // ---------------------------------------------------------------------------
218
+ // Tool handler implementations (exported for direct testing)
219
+ // ---------------------------------------------------------------------------
220
+
221
+ export async function handleScanHost(args) {
222
+ if (!args?.host || typeof args.host !== 'string') {
223
+ throw new Error('Missing required parameter: host');
224
+ }
225
+ const host = await _validateHostFn(args.host);
226
+
227
+ const pm = await getPluginManager();
228
+ // Note: timeout is controlled via PLUGIN_TIMEOUT_MS env var at startup.
229
+ // Runtime override is not supported to avoid process-global state mutation.
230
+ const output = await pm.run(host, 'all');
231
+ return {
232
+ host: output.host,
233
+ conclusion: output.conclusion ?? null,
234
+ manifest: output.manifest ?? [],
235
+ pluginsRan: output.results?.length ?? 0,
236
+ };
237
+ }
238
+
239
+ export async function handleProbeService(args) {
240
+ if (!args?.host || typeof args.host !== 'string') {
241
+ throw new Error('Missing required parameter: host');
242
+ }
243
+ if (args.port == null || typeof args.port !== 'number') {
244
+ throw new Error('Missing required parameter: port');
245
+ }
246
+ if (!args?.pluginName || typeof args.pluginName !== 'string') {
247
+ throw new Error('Missing required parameter: pluginName');
248
+ }
249
+ const host = await _validateHostFn(args.host);
250
+ validatePort(args.port);
251
+
252
+ const pm = await getPluginManager();
253
+ const plugin = pm.findPlugin(args.pluginName);
254
+ if (!plugin) {
255
+ throw new Error(`Unknown plugin: ${args.pluginName}`);
256
+ }
257
+
258
+ const result = await pm._runOne(plugin, host, args.port);
259
+ return result;
260
+ }
261
+
262
+ export async function handleGetVulnerabilities(args) {
263
+ if (!args?.cpe || typeof args.cpe !== 'string') {
264
+ throw new Error('Missing required parameter: cpe');
265
+ }
266
+ if (!/^cpe:2\.3:[aho]:/.test(args.cpe)) {
267
+ throw new Error('Invalid CPE 2.3 format. Expected: cpe:2.3:{a|h|o}:vendor:product:...');
268
+ }
269
+ if (args.cpe.length > 500) {
270
+ throw new Error('CPE string too long (max 500 characters)');
271
+ }
272
+
273
+ const client = await getNvdClient();
274
+ let cves = await client.queryCvesByCpe(args.cpe);
275
+
276
+ if (args.maxResults && typeof args.maxResults === 'number' && args.maxResults > 0) {
277
+ cves = cves.slice(0, args.maxResults);
278
+ }
279
+
280
+ return { cpe: args.cpe, totalResults: cves.length, cves };
281
+ }
282
+
283
+ export async function handleListPlugins() {
284
+ const pm = await getPluginManager();
285
+ const meta = pm.getAllPluginsMetadata();
286
+ return meta.map((p) => ({
287
+ id: p.id,
288
+ name: p.name,
289
+ priority: p.priority ?? null,
290
+ requirements: p.requirements ?? {},
291
+ }));
292
+ }
293
+
294
+ /** Map tool name to handler. Exported for testing. */
295
+ export const toolHandlers = {
296
+ scan_host: handleScanHost,
297
+ probe_service: handleProbeService,
298
+ get_vulnerabilities: handleGetVulnerabilities,
299
+ list_plugins: handleListPlugins,
300
+ };
301
+
302
+ // ---------------------------------------------------------------------------
303
+ // Server factory (exported for testing without starting transport)
304
+ // ---------------------------------------------------------------------------
305
+
306
+ export function createServer() {
307
+ const server = new Server(
308
+ {
309
+ name: 'nsauditor-mcp',
310
+ version: TOOL_VERSION,
311
+ },
312
+ {
313
+ capabilities: {
314
+ tools: {},
315
+ },
316
+ },
317
+ );
318
+
319
+ // --- List tools ---
320
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
321
+ tools: TOOLS,
322
+ }));
323
+
324
+ // --- Call tool ---
325
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
326
+ const { name, arguments: args } = request.params;
327
+
328
+ const handler = toolHandlers[name];
329
+ if (!handler) {
330
+ return {
331
+ content: [{ type: 'text', text: JSON.stringify({ error: `Unknown tool: ${name}` }) }],
332
+ isError: true,
333
+ };
334
+ }
335
+
336
+ // Gate Pro-tier tools at the MCP dispatch layer
337
+ if (name === 'probe_service' || name === 'get_vulnerabilities') {
338
+ const denied = requireProCapability(name);
339
+ if (denied) return denied;
340
+ }
341
+
342
+ try {
343
+ const result = await handler(args ?? {});
344
+
345
+ // Append tier info to list_plugins response
346
+ if (name === 'list_plugins') {
347
+ const tierLabel = { ce: 'Community Edition (CE)', pro: 'Pro', enterprise: 'Enterprise' };
348
+ const tierSuffix = `\n\nCurrent tier: ${tierLabel[_tier] ?? _tier}. ${_capabilities.proMCP ? '' : 'Upgrade to Pro for probe_service, get_vulnerabilities, risk_summary, and more.'}`;
349
+ return {
350
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) + tierSuffix }],
351
+ };
352
+ }
353
+
354
+ return {
355
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
356
+ };
357
+ } catch (err) {
358
+ return {
359
+ content: [{ type: 'text', text: JSON.stringify({ error: err.message }) }],
360
+ isError: true,
361
+ };
362
+ }
363
+ });
364
+
365
+ return server;
366
+ }
367
+
368
+ // ---------------------------------------------------------------------------
369
+ // Standalone entry point
370
+ // ---------------------------------------------------------------------------
371
+
372
+ const isMainModule =
373
+ typeof process !== 'undefined' &&
374
+ process.argv[1] &&
375
+ (process.argv[1].endsWith('mcp_server.mjs') ||
376
+ process.argv[1].endsWith('mcp_server'));
377
+
378
+ if (isMainModule) {
379
+ const server = createServer();
380
+ const transport = new StdioServerTransport();
381
+ await server.connect(transport);
382
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "nsauditor-ai",
3
+ "version": "0.1.0",
4
+ "description": "Modular AI-assisted network security audit platform — Community Edition",
5
+ "type": "module",
6
+ "private": false,
7
+ "scripts": {
8
+ "start": "node cli.mjs",
9
+ "test": "node --test",
10
+ "mcp": "node mcp_server.mjs"
11
+ },
12
+ "bin": {
13
+ "nsauditor-ai": "bin/nsauditor-ai.mjs",
14
+ "nsauditor-ai-mcp": "bin/nsauditor-ai-mcp.mjs"
15
+ },
16
+ "dependencies": {
17
+ "@anthropic-ai/sdk": "^0.82.0",
18
+ "@modelcontextprotocol/sdk": "^1.29.0",
19
+ "dnssd": "^0.4.1",
20
+ "dotenv": "^17.2.1",
21
+ "markdown-it": "^14.1.0",
22
+ "multicast-dns": "^7.2.5",
23
+ "node-upnp-utils": "^1.0.3",
24
+ "openai": "^4.104.0",
25
+ "oui-data": "^1.1.427",
26
+ "simple-wappalyzer": "^1.1.75",
27
+ "snmp-native": "^1.2.0",
28
+ "uuid": "^13.0.0",
29
+ "xml2js": "^0.6.2"
30
+ },
31
+ "optionalDependencies": {
32
+ "mdns": "^2.7.2"
33
+ },
34
+ "engines": {
35
+ "node": ">=20.0.0"
36
+ },
37
+ "license": "MIT",
38
+ "homepage": "https://www.nsauditor.com/ai/",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "https://github.com/nsasoft/nsauditor-ai.git"
42
+ },
43
+ "keywords": ["network", "security", "audit", "scanner", "vulnerability", "mcp"]
44
+ }