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
@@ -0,0 +1,290 @@
1
+ // plugins/http_probe.mjs
2
+ // Robust HTTP/HTTPS probe with tolerant parsing and vendor/model/firmware hints.
3
+ // Reads INSECURE_HTTPS=true to allow self-signed certs on router/printer UIs.
4
+
5
+ import http from 'node:http';
6
+ import https from 'node:https';
7
+ import { promisify } from 'node:util';
8
+ import { execFile } from 'node:child_process';
9
+
10
+ const execFileP = promisify(execFile);
11
+
12
+ const DEBUG = /^(1|true|yes|on)$/i.test(String(process.env.DEBUG_MODE || process.env.HTTP_DEBUG || ""));
13
+ function dlog(...a) { if (DEBUG) console.log("[http-probe]", ...a); }
14
+
15
+ function buildBanner(status, headers) {
16
+ const lines = [];
17
+ lines.push(`${status.code} ${status.message}`);
18
+ // Capture the most useful fingerprinting headers
19
+ const pick = [
20
+ 'www-authenticate',
21
+ 'server',
22
+ 'x-frame-options',
23
+ 'content-type',
24
+ 'set-cookie',
25
+ 'location', // include redirect target if any
26
+ ];
27
+ for (const k of pick) {
28
+ const v = headers[k];
29
+ if (!v) continue;
30
+ if (Array.isArray(v)) {
31
+ for (const vv of v) lines.push(`${k}: ${vv}`);
32
+ } else {
33
+ lines.push(`${k}: ${v}`);
34
+ }
35
+ }
36
+ return lines.join('\r\n').slice(0, 512); // Limit banner size
37
+ }
38
+
39
+ function parseNetgear(headers) {
40
+ const wa = headers['www-authenticate'];
41
+ if (!wa) return null;
42
+ const s = Array.isArray(wa) ? wa.join(' ') : wa;
43
+ // Examples: Basic realm="NETGEAR R8000" OR Basic realm="NETGEAR"
44
+ const m = /NETGEAR\s*([A-Za-z0-9\-]+)?/i.exec(s);
45
+ if (!m) return null;
46
+ const model = m[1] ? m[1].toUpperCase() : undefined;
47
+ return { vendor: 'NETGEAR', model };
48
+ }
49
+
50
+ function parseEpson(headers) {
51
+ const server = headers['server'];
52
+ const wa = headers['www-authenticate'];
53
+ const s = [server, wa].flat().filter(Boolean).join(' ');
54
+ if (!s) return null;
55
+ if (/epson/i.test(s)) {
56
+ // We don't usually get the model via HTTP alone; SNMP will fill that.
57
+ return { vendor: 'EPSON' };
58
+ }
59
+ return null;
60
+ }
61
+
62
+ async function probeOptions(host, port, isHttps, allowInsecure, timeoutMs) {
63
+ const mod = isHttps ? https : http;
64
+ const agent = isHttps ? new https.Agent({ rejectUnauthorized: !allowInsecure }) : undefined;
65
+
66
+ return new Promise((resolve) => {
67
+ const req = mod.request({
68
+ host, port,
69
+ method: 'OPTIONS',
70
+ path: '/',
71
+ headers: { 'User-Agent': 'nsauditor/0.1 (+http)' },
72
+ agent,
73
+ }, (res) => {
74
+ const allow = res.headers['allow'] || '';
75
+ res.resume();
76
+ resolve({ allow, status: res.statusCode });
77
+ });
78
+
79
+ req.setTimeout(timeoutMs, () => {
80
+ req.destroy(new Error('timeout'));
81
+ });
82
+ req.on('error', () => resolve(null));
83
+ req.end();
84
+ });
85
+ }
86
+
87
+ async function probeFirmware(host, vendor) {
88
+ if (!vendor || !vendor.toLowerCase().includes('netgear')) return null;
89
+ const cmd = "curl";
90
+ const args = [
91
+ "-s", "-m", "5",
92
+ `--connect-timeout`, "3",
93
+ `http://${host}/currentsetting.htm`
94
+ ];
95
+ try {
96
+ const { stdout } = await execFileP(cmd, args, { windowsHide: true, timeout: 6000 });
97
+ // Match formats like Firmware=V1.0.5.88_10.1.88 or Firmware Version=V1.0.5.88
98
+ const firmwareMatch = stdout.match(/Firmware(?:\s*Version)?=V?([\d._]+)/i);
99
+ if (firmwareMatch) {
100
+ dlog(`Firmware detected for ${host}: ${firmwareMatch[1]}`);
101
+ return firmwareMatch[1];
102
+ }
103
+ return null;
104
+ } catch (e) {
105
+ dlog(`Firmware probe error for ${host}: ${e.message}`);
106
+ return null;
107
+ }
108
+ }
109
+
110
+ export default {
111
+ id: '006',
112
+ name: 'HTTP Probe',
113
+ description: 'Probes HTTP/HTTPS (80,443) and extracts headers to fingerprint devices (e.g., routers/printers) with firmware detection.',
114
+ priority: 60,
115
+ requirements: { host: "up", tcp_open: [80, 443] },
116
+ protocols: ['tcp'],
117
+ ports: [80, 443],
118
+
119
+ async run(host, port = 80, opts = {}) {
120
+ const isHttps = Number(port) === 443;
121
+ const allowInsecure =
122
+ String(opts.insecureHttps ?? process.env.INSECURE_HTTPS ?? '').toLowerCase() === 'true';
123
+ const timeoutMs = Number(process.env.HTTP_TIMEOUT_MS || 6000);
124
+
125
+ const agent = isHttps ? new https.Agent({ rejectUnauthorized: !allowInsecure }) : undefined;
126
+ const mod = isHttps ? https : http;
127
+
128
+ const reqOpts = {
129
+ host,
130
+ port,
131
+ method: 'GET', // GET for maximal compatibility; we’ll ignore the body
132
+ path: '/',
133
+ headers: {
134
+ 'User-Agent': 'nsauditor/0.1 (+http)',
135
+ Accept: '*/*',
136
+ Connection: 'close',
137
+ },
138
+ agent,
139
+ };
140
+
141
+ const result = {
142
+ up: false,
143
+ program: null, // e.g., 'NETGEAR R8000 HTTP Server'
144
+ version: null, // e.g., 'V1.0.5.88'
145
+ os: null, // e.g., 'NETGEAR Router OS (Embedded Linux, Firmware V1.0.5.88)'
146
+ type: null, // e.g., 'router', 'printer'
147
+ data: []
148
+ };
149
+
150
+ // Perform the HTTP/HTTPS probe
151
+ const status = await new Promise((resolve) => {
152
+ const req = mod.request(reqOpts, async (res) => {
153
+ const headers = res.headers;
154
+ const status = { code: res.statusCode, message: res.statusMessage };
155
+
156
+ // Initial "up" via response
157
+ result.up = true;
158
+
159
+ // Vendor/model detection
160
+ const ngear = parseNetgear(headers);
161
+ const epson = parseEpson(headers);
162
+
163
+ if (ngear) {
164
+ result.program = `NETGEAR ${ngear.model || ''} HTTP Server`.trim();
165
+ result.type = 'router';
166
+ result.os = 'NETGEAR Router OS (Embedded Linux)';
167
+ } else if (epson) {
168
+ result.program = 'EPSON HTTP Server';
169
+ result.type = 'printer';
170
+ result.os = 'EPSON Printer OS (Embedded)';
171
+ }
172
+
173
+ // Probe for firmware if router detected
174
+ if (result.type === 'router' && ngear) {
175
+ const vendor = opts.context?.lookupVendor ? opts.context.lookupVendor(opts.context.arpMac) : ngear.vendor;
176
+ const firmware = await probeFirmware(host, vendor);
177
+ if (firmware) {
178
+ result.version = firmware;
179
+ result.os = `${result.os}, Firmware ${firmware}`;
180
+ result.data.push({
181
+ probe_protocol: isHttps ? 'https' : 'http',
182
+ probe_port: port,
183
+ probe_info: `Firmware detected: ${firmware}`,
184
+ response_banner: null
185
+ });
186
+ }
187
+ }
188
+
189
+ // Fallback to Server header if program still unknown
190
+ const server = headers['server'];
191
+ if (!result.program && server) {
192
+ result.program = String(server);
193
+ }
194
+
195
+ // If Epson is detected via headers, hint "printer" and stable program label
196
+ if (epson) {
197
+ result.type = 'printer';
198
+ if (!result.program || /unknown/i.test(result.program)) {
199
+ result.program = 'EPSON HTTP Server';
200
+ }
201
+ }
202
+
203
+ // Push a condensed "banner" capture for the probe
204
+ result.data.push({
205
+ probe_protocol: isHttps ? 'https' : 'http',
206
+ probe_port: port,
207
+ probe_info: ngear
208
+ ? `Detected router: ${result.program}`
209
+ : epson
210
+ ? `Detected printer: ${result.program}`
211
+ : server
212
+ ? `Server: ${server}`
213
+ : 'HTTP service detected',
214
+ response_banner: buildBanner(status, headers),
215
+ });
216
+
217
+ // Drain and finish
218
+ res.resume();
219
+ resolve(status);
220
+ });
221
+
222
+ req.setTimeout(timeoutMs, () => {
223
+ req.destroy(new Error('timeout'));
224
+ });
225
+
226
+ req.on('error', (err) => {
227
+ // Tolerate parse/TLS quirks by marking as up when we can infer service presence
228
+ const msg = String(err.message).toLowerCase();
229
+ const looksLikeUp =
230
+ msg.includes('certificate') ||
231
+ msg.includes('self-signed') ||
232
+ msg.includes('parse error') ||
233
+ msg.includes('write epipe') ||
234
+ msg.includes('unexpected server response'); // common TLS handshake noise
235
+
236
+ result.up = looksLikeUp || result.up;
237
+
238
+ // Record a minimal banner/error line so the operator sees context
239
+ result.data.push({
240
+ probe_protocol: isHttps ? 'https' : 'http',
241
+ probe_port: port,
242
+ probe_info: looksLikeUp
243
+ ? 'HTTP(S) reachable with TLS/parse issues (likely admin UI present)'
244
+ : `HTTP(S) error: ${err.message}`,
245
+ response_banner: null,
246
+ });
247
+
248
+ resolve({ code: 0, message: err.message });
249
+ });
250
+
251
+ req.end();
252
+ });
253
+
254
+ // Final heuristics if type is still unknown
255
+ if (!result.type && result.program) {
256
+ const p = result.program.toLowerCase();
257
+ if (p.includes('netgear')) result.type = 'router';
258
+ else if (p.includes('epson')) result.type = 'printer';
259
+ }
260
+
261
+ // Keep OS null unless we have a trustworthy hint; HTTP headers rarely reveal OS reliably.
262
+
263
+ // HTTP method testing via OPTIONS
264
+ try {
265
+ const optResult = await probeOptions(host, port, isHttps, allowInsecure, timeoutMs);
266
+ if (optResult?.allow) {
267
+ const methods = optResult.allow.split(',').map(m => m.trim().toUpperCase()).filter(Boolean);
268
+ const dangerous = methods.filter(m => ['PUT', 'DELETE', 'TRACE', 'CONNECT'].includes(m));
269
+ result.allowedMethods = methods;
270
+ result.dangerousMethods = dangerous;
271
+ if (dangerous.length > 0) {
272
+ result.data.push({
273
+ probe_protocol: isHttps ? 'https' : 'http',
274
+ probe_port: port,
275
+ probe_info: `WARNING: Dangerous HTTP methods enabled: ${dangerous.join(', ')}`,
276
+ response_banner: `Allow: ${optResult.allow}`
277
+ });
278
+ }
279
+ } else {
280
+ result.allowedMethods = [];
281
+ result.dangerousMethods = [];
282
+ }
283
+ } catch {
284
+ result.allowedMethods = [];
285
+ result.dangerousMethods = [];
286
+ }
287
+
288
+ return result;
289
+ },
290
+ };
@@ -0,0 +1,130 @@
1
+ // LLMNR Scanner — probes Link-Local Multicast Name Resolution (UDP/5355)
2
+ // Discovers hosts/services responding to LLMNR queries on the local network.
3
+
4
+ import dgram from "node:dgram";
5
+ import crypto from "node:crypto";
6
+
7
+ function buildLlmnrQuery(hostname) {
8
+ // Build a minimal LLMNR query packet for the given hostname (type A)
9
+ // See RFC 4795 for details
10
+ const tid = crypto.randomBytes(2); // Transaction ID
11
+ const flags = Buffer.from([0x00, 0x00]); // Standard query
12
+ const qdcount = Buffer.from([0x00, 0x01]); // One question
13
+ const ancount = Buffer.from([0x00, 0x00]);
14
+ const nscount = Buffer.from([0x00, 0x00]);
15
+ const arcount = Buffer.from([0x00, 0x00]);
16
+
17
+ // Encode hostname as DNS name
18
+ const labels = hostname.split('.').map(l => {
19
+ const b = Buffer.from(l, 'utf8');
20
+ return Buffer.concat([Buffer.from([b.length]), b]);
21
+ });
22
+ const qname = Buffer.concat([...labels, Buffer.from([0x00])]);
23
+ const qtype = Buffer.from([0x00, 0x01]); // Type A
24
+ const qclass = Buffer.from([0x00, 0x01]); // IN
25
+
26
+ return Buffer.concat([
27
+ tid, flags, qdcount, ancount, nscount, arcount,
28
+ qname, qtype, qclass
29
+ ]);
30
+ }
31
+
32
+ function parseLlmnrResponse(msg) {
33
+ // Parse minimal LLMNR response for A records
34
+ // Returns array of { name, address }
35
+ const results = [];
36
+ try {
37
+ // Skip header (12 bytes)
38
+ let offset = 12;
39
+ // Parse question (skip)
40
+ while (msg[offset] !== 0) offset++;
41
+ offset += 5; // null + type(2) + class(2)
42
+ // Parse answer(s)
43
+ while (offset < msg.length) {
44
+ // Name (pointer or label)
45
+ if ((msg[offset] & 0xc0) === 0xc0) {
46
+ offset += 2;
47
+ } else {
48
+ while (msg[offset] !== 0) offset++;
49
+ offset++;
50
+ }
51
+ const type = msg.readUInt16BE(offset); offset += 2;
52
+ const cls = msg.readUInt16BE(offset); offset += 2;
53
+ const ttl = msg.readUInt32BE(offset); offset += 4;
54
+ const rdlen = msg.readUInt16BE(offset); offset += 2;
55
+ if (type === 1 && cls === 1 && rdlen === 4) { // A record
56
+ const ip = Array.from(msg.slice(offset, offset + 4)).join('.');
57
+ results.push({ address: ip });
58
+ }
59
+ offset += rdlen;
60
+ }
61
+ } catch {}
62
+ return results;
63
+ }
64
+
65
+ export default {
66
+ id: "017",
67
+ name: "LLMNR Scanner",
68
+ description: "Probes Link-Local Multicast Name Resolution (UDP/5355) for host discovery and name resolution.",
69
+ priority: 346,
70
+ protocols: ["llmnr"],
71
+ ports: [5355],
72
+ requirements: {},
73
+ runStrategy: "single",
74
+
75
+ async run(host, _port = 0, opts = {}) {
76
+ const timeoutMs = Number(opts.timeoutMs ?? process.env.LLMNR_SCANNER_TIMEOUT_MS ?? 4000);
77
+ const hostname = opts?.hostname || host;
78
+ const query = buildLlmnrQuery(hostname);
79
+
80
+ const data = [];
81
+ const responses = new Set();
82
+
83
+ const sock = dgram.createSocket("udp4");
84
+ sock.bind();
85
+
86
+ // Listen for responses
87
+ sock.on("message", (msg, rinfo) => {
88
+ // Only process responses from the target host
89
+ if (rinfo.address !== host) return;
90
+
91
+ const parsed = parseLlmnrResponse(msg);
92
+ for (const rec of parsed) {
93
+ const key = `${rinfo.address}|${rec.address}`;
94
+ if (!responses.has(key)) {
95
+ responses.add(key);
96
+ data.push({
97
+ probe_protocol: "llmnr",
98
+ probe_port: 5355,
99
+ probe_info: `LLMNR response from ${rinfo.address}`,
100
+ response_banner: JSON.stringify(rec)
101
+ });
102
+ }
103
+ }
104
+ });
105
+
106
+ // Send query to LLMNR multicast address
107
+ sock.send(query, 5355, "224.0.0.252");
108
+
109
+ await new Promise(res => setTimeout(res, timeoutMs));
110
+ try { sock.close(); } catch {}
111
+
112
+ if (data.length === 0) {
113
+ data.push({
114
+ probe_protocol: "llmnr",
115
+ probe_port: 5355,
116
+ probe_info: "No LLMNR response observed in timeout window",
117
+ response_banner: null
118
+ });
119
+ }
120
+
121
+ return {
122
+ up: data.length > 1,
123
+ program: "LLMNR",
124
+ version: "RFC4795",
125
+ os: null,
126
+ type: "llmnr",
127
+ data
128
+ };
129
+ }
130
+ };