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,162 @@
1
+ // plugins/arp_scanner.mjs
2
+ // ARP Scanner — infers vendor/OS hints from ARP (local targets only) via ctx helpers.
3
+ // Short-circuits if a prior plugin already inferred OS (e.g., Ping Checker or others).
4
+
5
+ import { promisify } from "node:util";
6
+ import { execFile } from "node:child_process";
7
+ import { isPrivateLike } from '../utils/net_validation.mjs';
8
+
9
+ const execFileP = promisify(execFile);
10
+
11
+ const DEBUG = /^(1|true|yes|on)$/i.test(String(process.env.DEBUG_MODE || process.env.ARP_DEBUG || ""));
12
+ const ONLY_IF_OS_UNKNOWN = !/^(0|false|no|off)$/i.test(String(process.env.ARP_RUN_ONLY_IF_OS_UNKNOWN ?? "1"));
13
+
14
+ function dlog(...a) { if (DEBUG) console.log("[arp-scanner]", ...a); }
15
+
16
+ // --- Exported: tests expect a STRING MAC (or null) ---
17
+ export function parseArpOutput(out, ip) {
18
+ if (!out) return null;
19
+
20
+ // Normalize MAC patterns (colon or dash, 1 or 2 hex digits per octet)
21
+ const macRe = /([0-9a-f]{1,2}(?:[:-][0-9a-f]{1,2}){5})/i;
22
+
23
+ // Log raw output for debugging
24
+ dlog("Raw ARP output:", out);
25
+
26
+ // Prefer a line with the specific IP if provided (macOS/Linux: IP in parentheses or first column; Windows: "Internet Address")
27
+ if (ip) {
28
+ const ipLine = out
29
+ .split(/\r?\n/)
30
+ .find((l) => l.includes(ip) || l.includes(`(${ip})`));
31
+ if (ipLine) {
32
+ const m = ipLine.match(macRe);
33
+ if (m) {
34
+ let mac = m[1].replace(/-/g, ":").toUpperCase();
35
+ // Normalize to two digits per octet
36
+ mac = mac.split(":").map(part => part.padStart(2, "0")).join(":");
37
+ dlog(`Parsed MAC for IP ${ip}: ${mac}`);
38
+ return mac;
39
+ }
40
+ }
41
+ }
42
+
43
+ // Fallback: first MAC in the whole output
44
+ const m2 = out.match(macRe);
45
+ if (m2) {
46
+ let mac = m2[1].replace(/-/g, ":").toUpperCase();
47
+ mac = mac.split(":").map(part => part.padStart(2, "0")).join(":");
48
+ dlog(`Parsed fallback MAC: ${mac}`);
49
+ return mac;
50
+ }
51
+
52
+ dlog("No MAC found in output");
53
+ return null;
54
+ }
55
+
56
+ async function getMacViaArp(ip, iface = null) {
57
+ const cmd = "arp";
58
+ const args = process.platform === "win32" ? ["-a", ip] : ["-n", ip];
59
+ // Add interface specification for macOS/Linux if provided
60
+ if (iface && process.platform !== "win32") {
61
+ args.push("-i", iface);
62
+ }
63
+
64
+ try {
65
+ const { stdout } = await execFileP(cmd, args, { windowsHide: true, timeout: 5000 });
66
+ return parseArpOutput(stdout, ip);
67
+ } catch (e) {
68
+ dlog("arp exec failed:", e?.message || e);
69
+ return null;
70
+ }
71
+ }
72
+
73
+ export default {
74
+ id: "026",
75
+ name: "ARP Scanner",
76
+ description: "Infers vendor/OS hints from ARP (local targets only) via ctx helpers.",
77
+ priority: 25,
78
+ requirements: {},
79
+ protocols: ["arp"],
80
+ ports: [],
81
+
82
+ async run(host, _port, opts = {}) {
83
+ const data = [];
84
+ let up = false;
85
+ let os = null;
86
+
87
+ // ----- Short-circuit logic (no manager changes required) -----
88
+ const ctx = opts?.context || {};
89
+ const osKnown = !!ctx.os || !!ctx.arpOs || !!ctx.pingOs;
90
+ if (ONLY_IF_OS_UNKNOWN && osKnown) {
91
+ dlog("Skipping ARP scan — OS already known by prior probe");
92
+ data.push({
93
+ probe_protocol: "arp",
94
+ probe_port: 0,
95
+ probe_info: "Skipped: OS already known from prior plugin",
96
+ response_banner: null
97
+ });
98
+ return { up: false, os: null, data };
99
+ }
100
+ // -------------------------------------------------------------
101
+
102
+ // Only meaningful on local subnets
103
+ if (!isPrivateLike(host)) {
104
+ dlog("Target not in private/local range; ARP not attempted.");
105
+ data.push({
106
+ probe_protocol: "arp",
107
+ probe_port: 0,
108
+ probe_info: "Non-local target — ARP not attempted",
109
+ response_banner: null
110
+ });
111
+ return { up: false, os: null, data };
112
+ }
113
+
114
+ // Try with interface 'en0' for macOS, fallback to no interface
115
+ const interfaces = process.platform === "win32" ? [null] : [null, "en0"];
116
+ let mac = null;
117
+ for (const iface of interfaces) {
118
+ mac = await getMacViaArp(host, iface);
119
+ if (mac) break;
120
+ }
121
+
122
+ if (!mac) {
123
+ data.push({
124
+ probe_protocol: "arp",
125
+ probe_port: 0,
126
+ probe_info: "No ARP entry",
127
+ response_banner: null,
128
+ mac: null
129
+ });
130
+ return { up: false, os: null, data };
131
+ }
132
+
133
+ up = true; // ARP implies L2 presence
134
+
135
+ // Use ctx helpers only (no internal OUI loading)
136
+ const vendorRaw = typeof ctx.lookupVendor === "function" ? (ctx.lookupVendor(mac) || null) : null;
137
+ const vendor = vendorRaw ? vendorRaw.replace(/[\r\n]+/g, ' ').trim() : null;
138
+ const info = vendor ? `ARP entry found — vendor: ${vendor}` : "ARP entry found";
139
+ data.push({
140
+ probe_protocol: "arp",
141
+ probe_port: 0,
142
+ probe_info: info,
143
+ response_banner: mac,
144
+ mac
145
+ });
146
+
147
+ // Vendor → OS heuristic via ctx helper (conservative)
148
+ if (typeof ctx.probableOsFromVendor === "function") {
149
+ const vendorOs = ctx.probableOsFromVendor(vendor);
150
+ if (vendorOs && vendorOs !== "Unknown") os = vendorOs;
151
+ }
152
+
153
+ return {
154
+ up,
155
+ program: "ARP",
156
+ version: "Unknown",
157
+ os,
158
+ type: "arp",
159
+ data
160
+ };
161
+ }
162
+ };
@@ -0,0 +1,248 @@
1
+ // plugins/db_scanner.mjs
2
+ // Lightweight DB product/version probes (no external deps). Best-effort banner grabs.
3
+ //
4
+ // Detects: MySQL/MariaDB (3306), Microsoft SQL Server (1433), PostgreSQL (5432),
5
+ // Oracle TNS (1521), MongoDB (27017)
6
+
7
+ import net from 'node:net';
8
+
9
+ const TIMEOUT = 2500;
10
+ const DB_PORTS = [3306, 1433, 5432, 1521, 27017];
11
+
12
+ function connect(host, port, timeoutMs) {
13
+ return new Promise((resolve, reject) => {
14
+ const s = new net.Socket();
15
+ let done = false;
16
+ const finish = (err, sock) => { if (done) return; done = true; err ? reject(err) : resolve(sock); };
17
+ s.setNoDelay(true);
18
+ s.setTimeout(timeoutMs, () => { try{s.destroy()}catch{}; finish(new Error('timeout')); });
19
+ s.once('error', (e)=>{ try{s.destroy()}catch{}; finish(e); });
20
+ s.connect(port, host, ()=>finish(null, s));
21
+ });
22
+ }
23
+
24
+ const readAll = (sock, ms) => new Promise(res => {
25
+ const chunks = []; let len = 0; const t = setTimeout(() => end(), ms);
26
+ const end = () => { clearTimeout(t); try{sock.destroy()}catch{}; res(Buffer.concat(chunks, len)); };
27
+ sock.on('data', b => { chunks.push(b); len += b.length; if (len > 65536) end(); });
28
+ sock.on('end', end); sock.on('close', end); sock.on('error', end);
29
+ });
30
+
31
+ // --- MySQL / MariaDB (3306) ---
32
+ async function probeMySQL(host, port, timeoutMs) {
33
+ const s = await connect(host, port, timeoutMs);
34
+ const buf = await readAll(s, timeoutMs); // server greets first
35
+ if (buf.length >= 6 && buf[4] === 0x0a) {
36
+ let i = 5;
37
+ while (i < buf.length && buf[i] !== 0x00) i++;
38
+ const verStr = buf.slice(5, i).toString('utf8');
39
+ const lower = verStr.toLowerCase();
40
+ const product = lower.includes('mariadb') ? 'MariaDB' : 'MySQL';
41
+ const m = verStr.match(/\d+\.\d+(?:\.\d+)?/);
42
+ return { type: 'mysql', product, version: m ? m[0] : '', banner: `MySQL greeting: ${verStr}` };
43
+ }
44
+ return { type: 'mysql', product: 'MySQL', version: '', banner: 'MySQL (no greeting parsed)' };
45
+ }
46
+
47
+ // --- Microsoft SQL Server / TDS (1433) ---
48
+ async function probeMSSQL(host, port, timeoutMs) {
49
+ const s = await connect(host, port, timeoutMs);
50
+ // Minimal PRELOGIN request (TDS 7.x)
51
+ const prelogin = Buffer.from([
52
+ 0x12,0x01,0x00,0x14,0x00,0x00,0x00,0x00, // TDS header
53
+ 0x00,0x00,0x06,0x00,0x06, // VERSION token, offset 0x0006, len 6
54
+ 0xFF, // terminator
55
+ 0x08,0x00,0x01,0x55,0x00,0x00 // arbitrary client version payload
56
+ ]);
57
+ s.write(prelogin);
58
+ const buf = await readAll(s, timeoutMs);
59
+ const hdrLen = 8;
60
+ if (buf.length <= hdrLen) {
61
+ return { type: 'mssql', product: 'Microsoft SQL Server', version: '', banner: 'TDS (no response)' };
62
+ }
63
+ const payload = buf.slice(hdrLen);
64
+ let i = 0, version = '';
65
+ while (i + 4 < payload.length && payload[i] !== 0xFF) {
66
+ const token = payload[i];
67
+ const offset = payload.readUInt16BE(i+1);
68
+ const length = payload.readUInt16BE(i+3);
69
+ if (token === 0x00 && offset + length <= payload.length && length === 6) {
70
+ const v = payload.slice(offset, offset + length);
71
+ const major = v[0], minor = v[1], build = v.readUInt16BE(2), sub = v.readUInt16BE(4);
72
+ version = `${major}.${minor}.${build}.${sub}`;
73
+ break;
74
+ }
75
+ i += 5;
76
+ }
77
+ const map = { 16: 'SQL Server 2022', 15: 'SQL Server 2019', 14: 'SQL Server 2017', 13: 'SQL Server 2016', 12: 'SQL Server 2014', 11: 'SQL Server 2012', 10: 'SQL Server 2008/R2', 9: 'SQL Server 2005', 8: 'SQL Server 2000' };
78
+ const edition = version ? map[Number(version.split('.')[0])] || '' : '';
79
+ return {
80
+ type: 'mssql',
81
+ product: 'Microsoft SQL Server',
82
+ version,
83
+ banner: version ? `MSSQL PRELOGIN: ${version}${edition ? ' ('+edition+')' : ''}` : 'MSSQL (no version parsed)'
84
+ };
85
+ }
86
+
87
+ // --- PostgreSQL (5432) ---
88
+ async function probePostgres(host, port, timeoutMs) {
89
+ const s = await connect(host, port, timeoutMs);
90
+ // StartupMessage: len(4) + protocol 3.0 (4) + params (key\0val\0... \0)
91
+ const params = Buffer.from('user\0pgscan\0database\0postgres\0application_name\0audit-scan\0\0','utf8');
92
+ const startup = Buffer.alloc(8 + params.length);
93
+ startup.writeUInt32BE(startup.length, 0);
94
+ startup.writeUInt32BE(196608, 4); // 3.0
95
+ params.copy(startup, 8);
96
+ s.write(startup);
97
+ const buf = await readAll(s, timeoutMs);
98
+ const text = buf.toString('utf8');
99
+ const m = text.match(/PostgreSQL\s+(\d+\.\d+(?:\.\d+)?)/i);
100
+ const version = m ? m[1] : '';
101
+ return { type: 'postgresql', product: 'PostgreSQL', version, banner: version ? `PostgreSQL ${version}` : 'PostgreSQL (version not disclosed)' };
102
+ }
103
+
104
+ // --- Oracle TNS (1521) — heuristic ---
105
+ async function probeOracleTNS(host, port, timeoutMs) {
106
+ const s = await connect(host, port, timeoutMs);
107
+ const buf = await readAll(s, timeoutMs);
108
+ const b = buf.toString('utf8');
109
+ const m = b.match(/version\s*=?\s*([\d.]+)/i);
110
+ return { type: 'oracle', product: 'Oracle Database', version: m ? m[1] : '', banner: b || 'TNS (no banner)' };
111
+ }
112
+
113
+ // --- MongoDB (27017) ---
114
+ async function probeMongoDB(host, port, timeoutMs) {
115
+ const s = await connect(host, port, timeoutMs);
116
+ // Build OP_QUERY for { buildInfo: 1 } on admin.$cmd
117
+ const coll = Buffer.from('admin.$cmd\0', 'utf8');
118
+ const doc = buildBsonInt32('buildInfo', 1);
119
+ const headerLen = 16, bodyLen = 4 + coll.length + 4 + 4 + doc.length;
120
+ const buf = Buffer.alloc(headerLen + bodyLen);
121
+ let o = 0;
122
+ buf.writeInt32LE(headerLen + bodyLen, o); o+=4;
123
+ buf.writeInt32LE(1, o); o+=4; // requestID
124
+ buf.writeInt32LE(0, o); o+=4; // responseTo
125
+ buf.writeInt32LE(2004, o); o+=4; // OP_QUERY
126
+ buf.writeInt32LE(0, o); o+=4; // flags
127
+ coll.copy(buf, o); o += coll.length;
128
+ buf.writeInt32LE(0, o); o+=4; // numberToSkip
129
+ buf.writeInt32LE(-1, o); o+=4; // numberToReturn
130
+ doc.copy(buf, o); o += doc.length;
131
+ s.write(buf);
132
+ const rsp = await readAll(s, timeoutMs);
133
+ const txt = rsp.toString('utf8');
134
+ const m = txt.match(/"version"\s*:\s*"([^"]+)"/i);
135
+ const version = m ? m[1] : '';
136
+ return { type: 'mongodb', product: 'MongoDB', version, banner: version ? `MongoDB buildInfo: ${version}` : 'MongoDB (version not disclosed)' };
137
+ }
138
+
139
+ function buildBsonInt32(key, value) {
140
+ const k = Buffer.from(key + '\0', 'utf8');
141
+ const len = 4 + 1 + k.length + 4 + 1; // size + type + key + int32 + terminator
142
+ const b = Buffer.alloc(len);
143
+ let o = 0;
144
+ b[o++] = 0; // filled after
145
+ b[o++] = 0;
146
+ b[o++] = 0;
147
+ b[o++] = 0;
148
+ b[o++] = 0x10; // int32
149
+ k.copy(b, o); o += k.length;
150
+ b.writeInt32LE(value, o); o+=4;
151
+ b[o++] = 0x00;
152
+ b.writeInt32LE(len, 0);
153
+ return b;
154
+ }
155
+
156
+ async function probeDb(host, port, timeoutMs) {
157
+ switch (Number(port)) {
158
+ case 3306: return await probeMySQL(host, port, timeoutMs);
159
+ case 1433: return await probeMSSQL(host, port, timeoutMs);
160
+ case 5432: return await probePostgres(host, port, timeoutMs);
161
+ case 1521: return await probeOracleTNS(host, port, timeoutMs);
162
+ case 27017: return await probeMongoDB(host, port, timeoutMs);
163
+ default: return null;
164
+ }
165
+ }
166
+
167
+ export default {
168
+ id: '025',
169
+ name: 'DB Scanner',
170
+ description: 'Identifies common databases on well-known ports and extracts product/version banners.',
171
+ priority: 62,
172
+ requirements: { host: 'up', tcp_open: DB_PORTS },
173
+ protocols: ['tcp'],
174
+ ports: DB_PORTS,
175
+ dependencies: [],
176
+
177
+ async run(host, port = 0, opts = {}) {
178
+ const timeoutMs = Number(opts.timeoutMs || process.env.DB_SCAN_TIMEOUT_MS || TIMEOUT);
179
+ const result = {
180
+ up: false,
181
+ program: 'Unknown',
182
+ version: 'Unknown',
183
+ os: null,
184
+ type: 'database',
185
+ data: [],
186
+ };
187
+
188
+ try {
189
+ const r = await probeDb(host, Number(port), timeoutMs);
190
+ if (!r) {
191
+ result.data.push({
192
+ probe_protocol: 'tcp',
193
+ probe_port: Number(port),
194
+ probe_info: 'Unsupported DB port',
195
+ response_banner: null
196
+ });
197
+ return result;
198
+ }
199
+
200
+ const program =
201
+ r.product || (r.type ? String(r.type).toUpperCase() : 'Unknown');
202
+
203
+ result.up = true; // a TCP conversation succeeded
204
+ result.program = program;
205
+ result.version = r.version || 'Unknown';
206
+
207
+ result.data.push({
208
+ probe_protocol: 'tcp',
209
+ probe_port: Number(port),
210
+ probe_info: `${program}${r.version ? ' ' + r.version : ''}`.trim(),
211
+ response_banner: r.banner || null
212
+ });
213
+
214
+ return result;
215
+ } catch (e) {
216
+ result.data.push({
217
+ probe_protocol: 'tcp',
218
+ probe_port: Number(port),
219
+ probe_info: `TCP error: ${e?.message || e}`,
220
+ response_banner: null
221
+ });
222
+ return result;
223
+ }
224
+ }
225
+ };
226
+
227
+ import { statusFrom } from '../utils/conclusion_utils.mjs';
228
+
229
+ export async function conclude({ host, result }) {
230
+ const rows = Array.isArray(result?.data) ? result.data : [];
231
+ const svcByPort = { 3306:'mysql', 5432:'postgresql', 1433:'mssql', 1521:'oracle', 27017:'mongodb' };
232
+ const items = [];
233
+ for (const r of rows) {
234
+ if (typeof r?.probe_port !== 'number') continue;
235
+ const port = Number(r.probe_port);
236
+ const proto = String(r?.probe_protocol || 'tcp');
237
+ const service = svcByPort[port] || (proto === 'udp' ? `udp-${port}` : `tcp-${port}`);
238
+ const info = r?.probe_info || null;
239
+ const banner = r?.response_banner || null;
240
+ items.push({
241
+ port, protocol: proto, service,
242
+ program: result?.program || 'Unknown', version: result?.version || 'Unknown',
243
+ status: result?.up ? 'open' : statusFrom({ info, banner }),
244
+ info, banner, source: 'db-scanner', evidence: rows, authoritative: true
245
+ });
246
+ }
247
+ return items;
248
+ }