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,339 @@
1
+ // plugins/sunrpc_scanner.mjs
2
+ import net from 'node:net';
3
+ import dgram from 'node:dgram';
4
+
5
+ /* ----------------------------- Helpers ----------------------------- */
6
+
7
+ function xdrEncodeInt(buffer, value, offset) {
8
+ buffer.writeUInt32BE(value >>> 0, offset);
9
+ return offset + 4;
10
+ }
11
+
12
+ function xdrDecodeInt(buffer, offset) {
13
+ const value = buffer.readInt32BE(offset);
14
+ return { value, newOffset: offset + 4 };
15
+ }
16
+
17
+ // Robust parser that handles both simplified replies (no accept_stat) and proper RPC replies (with accept_stat).
18
+ function parseRpcPortFromReply(msg) {
19
+ // msg is expected to start at XID (no TCP record marker)
20
+ // Minimum valid RPC reply is 24 bytes
21
+ if (msg.length < 24) {
22
+ return null;
23
+ }
24
+
25
+ try {
26
+ // Layout:
27
+ // 0: xid
28
+ // 4: msg_type (should be 1 for REPLY)
29
+ // 8: reply_stat (should be 0 for ACCEPTED)
30
+ // 12: verifier_flavor
31
+ // 16: verifier_length
32
+ // 20: verifier_body (padded to 4) -> accept_stat -> result (port)
33
+ const msgType = msg.readUInt32BE(4);
34
+ const replyState = msg.readUInt32BE(8);
35
+ if (msgType !== 1 || replyState !== 0) return null;
36
+
37
+ // Compute offset after opaque_auth verifier (flavor,len,body[padded])
38
+ if (msg.length >= 20) {
39
+ const verflen = msg.readUInt32BE(16) >>> 0;
40
+ const pad = (4 - (verflen % 4)) % 4;
41
+ const acceptOff = 20 + verflen + pad;
42
+
43
+ // Proper RPC reply with accept_stat followed by result (port) on success
44
+ if (msg.length >= acceptOff + 8) {
45
+ const acceptStat = msg.readUInt32BE(acceptOff);
46
+ if (acceptStat === 0 /* SUCCESS */) {
47
+ const port = msg.readUInt32BE(acceptOff + 4);
48
+ return port > 0 && port <= 65535 ? port : null;
49
+ }
50
+ }
51
+ }
52
+
53
+ // Fallback: treat offset 20 as port directly (simplified responders)
54
+ if (msg.length >= 24) {
55
+ const port = msg.readUInt32BE(20);
56
+ return port > 0 && port <= 65535 ? port : null;
57
+ }
58
+ } catch {
59
+ return null;
60
+ }
61
+ return null;
62
+ }
63
+
64
+ async function getRpcPortTcp(host, program, version, timeout = 1000, rpcPort = 111) {
65
+ return new Promise((resolve, reject) => {
66
+ const socket = new net.Socket();
67
+ socket.setTimeout(timeout);
68
+ let receivedData = Buffer.alloc(0);
69
+ let expectedLength = null;
70
+ let done = false;
71
+
72
+ const finalize = (val) => {
73
+ if (done) return;
74
+ done = true;
75
+ clearTimeout(timeoutId);
76
+ socket.removeAllListeners();
77
+ try {
78
+ socket.end();
79
+ socket.destroy();
80
+ } catch {}
81
+ resolve(val);
82
+ };
83
+
84
+ let timeoutId = setTimeout(() => {
85
+ finalize(null);
86
+ }, timeout);
87
+
88
+ socket.on('error', () => {
89
+ finalize(null);
90
+ });
91
+
92
+ socket.on('timeout', () => {
93
+ finalize(null);
94
+ });
95
+
96
+ socket.on('close', () => {
97
+ // Connection closed. Try to parse any data we might have received.
98
+ if (expectedLength !== null && receivedData.length >= expectedLength + 4) {
99
+ const rpcMessage = receivedData.slice(4, expectedLength + 4);
100
+ const port = parseRpcPortFromReply(rpcMessage);
101
+ return finalize(port);
102
+ }
103
+ finalize(null);
104
+ });
105
+
106
+ socket.on('data', (data) => {
107
+ if (done) return;
108
+ receivedData = Buffer.concat([receivedData, data]);
109
+
110
+ // Parse TCP record marker
111
+ if (expectedLength === null && receivedData.length >= 4) {
112
+ const header = receivedData.readUInt32BE(0);
113
+ const lastFragment = (header & 0x80000000) !== 0;
114
+ expectedLength = header & 0x7FFFFFFF;
115
+
116
+ // Sanity checks
117
+ if (!lastFragment || expectedLength === 0 || expectedLength > 8192) {
118
+ return finalize(null);
119
+ }
120
+ }
121
+
122
+ // Check if we have complete message
123
+ if (expectedLength !== null && receivedData.length >= expectedLength + 4) {
124
+ const rpcMessage = receivedData.slice(4, expectedLength + 4);
125
+ const port = parseRpcPortFromReply(rpcMessage);
126
+ return finalize(port);
127
+ }
128
+ });
129
+
130
+ socket.connect(rpcPort, host, () => {
131
+ if (done) return;
132
+
133
+ const transactionId = Math.floor(Math.random() * 0xFFFFFFFF);
134
+ // Proper RPC CALL with cred/verifier (AUTH_NULL) + PMAP GETPORT args
135
+ const rpcMessage = Buffer.alloc(56); // 14 * 4-byte integers
136
+ let offset = 0;
137
+
138
+ // RPC call header
139
+ offset = xdrEncodeInt(rpcMessage, transactionId, offset); // xid
140
+ offset = xdrEncodeInt(rpcMessage, 0, offset); // CALL
141
+ offset = xdrEncodeInt(rpcMessage, 2, offset); // RPC version
142
+
143
+ const rpcbindProgram = 100000;
144
+ const rpcbindVersion = 2;
145
+ const getPortProcedure = 3;
146
+
147
+ offset = xdrEncodeInt(rpcMessage, rpcbindProgram, offset);
148
+ offset = xdrEncodeInt(rpcMessage, rpcbindVersion, offset);
149
+ offset = xdrEncodeInt(rpcMessage, getPortProcedure, offset);
150
+
151
+ // AUTH credentials (AUTH_NULL)
152
+ offset = xdrEncodeInt(rpcMessage, 0, offset); // cred flavor
153
+ offset = xdrEncodeInt(rpcMessage, 0, offset); // cred length
154
+
155
+ // AUTH verifier (AUTH_NULL)
156
+ offset = xdrEncodeInt(rpcMessage, 0, offset); // verf flavor
157
+ offset = xdrEncodeInt(rpcMessage, 0, offset); // verf length
158
+
159
+ // PMAP_GETPORT args: program, version, protocol, port(0)
160
+ offset = xdrEncodeInt(rpcMessage, program, offset);
161
+ offset = xdrEncodeInt(rpcMessage, version, offset);
162
+ offset = xdrEncodeInt(rpcMessage, 6, offset); // TCP
163
+ offset = xdrEncodeInt(rpcMessage, 0, offset);
164
+
165
+ const frameHeader = Buffer.alloc(4);
166
+ frameHeader.writeUInt32BE((rpcMessage.length | 0x80000000) >>> 0, 0);
167
+ const packet = Buffer.concat([frameHeader, rpcMessage]);
168
+
169
+ socket.write(packet, (err) => {
170
+ if (err && !done) {
171
+ finalize(null);
172
+ }
173
+ });
174
+ });
175
+ });
176
+ }
177
+
178
+ async function getRpcPortUdp(host, program, version, timeout = 1000, rpcPort = 111) {
179
+ return new Promise((resolve, reject) => {
180
+ const client = dgram.createSocket('udp4');
181
+ let done = false;
182
+ const finalize = (val) => {
183
+ if (done) return;
184
+ done = true;
185
+ clearTimeout(timeoutId);
186
+ client.removeAllListeners();
187
+ try { client.close(); } catch {}
188
+ resolve(val);
189
+ };
190
+
191
+ let timeoutId = setTimeout(() => {
192
+ finalize(null); // Timeout treated as normal failure case
193
+ }, timeout);
194
+
195
+ client.on('error', () => {
196
+ finalize(null); // Network errors treated as normal failure case
197
+ });
198
+
199
+ client.on('message', (msg) => {
200
+ if (done) return;
201
+ // UDP reply starts at XID (no record marker)
202
+ const port = parseRpcPortFromReply(msg);
203
+ finalize(port);
204
+ });
205
+
206
+ // Construct the rpcbind GETPORT packet with AUTH_NULL cred/verifier
207
+ const transactionId = Math.floor(Math.random() * 0xFFFFFFFF);
208
+ const packet = Buffer.alloc(56); // 14 * 4-byte integers
209
+ let offset = 0;
210
+
211
+ // RPC call header
212
+ offset = xdrEncodeInt(packet, transactionId, offset); // xid
213
+ offset = xdrEncodeInt(packet, 0, offset); // CALL
214
+ offset = xdrEncodeInt(packet, 2, offset); // RPC version
215
+
216
+ const rpcbindProgram = 100000;
217
+ const rpcbindVersion = 2;
218
+ const getPortProcedure = 3;
219
+
220
+ offset = xdrEncodeInt(packet, rpcbindProgram, offset);
221
+ offset = xdrEncodeInt(packet, rpcbindVersion, offset);
222
+ offset = xdrEncodeInt(packet, getPortProcedure, offset);
223
+
224
+ // AUTH credentials (AUTH_NULL)
225
+ offset = xdrEncodeInt(packet, 0, offset); // cred flavor
226
+ offset = xdrEncodeInt(packet, 0, offset); // cred length
227
+
228
+ // AUTH verifier (AUTH_NULL)
229
+ offset = xdrEncodeInt(packet, 0, offset); // verf flavor
230
+ offset = xdrEncodeInt(packet, 0, offset); // verf length
231
+
232
+ // PMAP_GETPORT args: program, version, protocol, port(0)
233
+ offset = xdrEncodeInt(packet, program, offset);
234
+ offset = xdrEncodeInt(packet, version, offset);
235
+ offset = xdrEncodeInt(packet, 17, offset); // UDP
236
+ offset = xdrEncodeInt(packet, 0, offset);
237
+
238
+ client.send(packet, rpcPort, host, (err) => {
239
+ if (err) {
240
+ finalize(null);
241
+ }
242
+ });
243
+ });
244
+ }
245
+
246
+ /* ----------------------------- Scanner Plugin ----------------------------- */
247
+
248
+ // Reduced set for faster scanning
249
+ const RPC_PROGRAMS = [
250
+ { num: 100000, name: 'PORTMAPPER', versions: [2] },
251
+ { num: 100003, name: 'NFS', versions: [3] },
252
+ { num: 100005, name: 'MOUNTD', versions: [1] }
253
+ ];
254
+
255
+ export default {
256
+ id: "015",
257
+ name: "SUN RPC Scanner",
258
+ description: "Scans for RPC services via portmapper on TCP/UDP 111 including NFS, mountd and others",
259
+ priority: 350,
260
+ requirements: {},
261
+ protocols: ["tcp", "udp"],
262
+ ports: [111],
263
+
264
+ async run(host, port=111, opts={}) {
265
+ const data = [];
266
+ let up = false;
267
+ let program = "Unknown";
268
+ let version = "Unknown";
269
+
270
+ // Determine which protocols to scan based on opts or default to both
271
+ const protocols = opts.protocol ? [opts.protocol] : ['tcp', 'udp'];
272
+ const timeout = opts.timeout || 1000;
273
+
274
+ // Scan each RPC program using provided port and protocols
275
+ for (const protocol of protocols) {
276
+ for (const prog of RPC_PROGRAMS) {
277
+ for (const ver of prog.versions) {
278
+ try {
279
+ const getRpcPort = protocol === 'tcp' ? getRpcPortTcp : getRpcPortUdp;
280
+ const detectedPort = await getRpcPort(host, prog.num, ver, timeout, port);
281
+
282
+ if (detectedPort) {
283
+ up = true;
284
+ program = "SUN RPC";
285
+
286
+ data.push({
287
+ // normalized evidence fields (preferred by reports)
288
+ from: 'sunrpc',
289
+ protocol,
290
+ port: detectedPort,
291
+ info: `RPC ${prog.name} v${ver} (${protocol.toUpperCase()})`,
292
+ banner: `Program ${prog.num} Version ${ver} on port ${detectedPort} via ${protocol.toUpperCase()}`,
293
+ status: 'open',
294
+ service: prog.name.toLowerCase(),
295
+
296
+ // keep legacy probe_* fields for compatibility
297
+ probe_protocol: protocol,
298
+ probe_port: detectedPort,
299
+ probe_info: `RPC ${prog.name} v${ver} (${protocol.toUpperCase()})`,
300
+ response_banner: `Program ${prog.num} Version ${ver} on port ${detectedPort} via ${protocol.toUpperCase()}`
301
+ });
302
+
303
+ // Exit early after finding first service to speed up testing
304
+ if (opts.protocol) return { up, program, version, type: 'sunrpc', data };
305
+ }
306
+ } catch {
307
+ // Individual program/version failures are ignored
308
+ }
309
+ }
310
+ }
311
+ }
312
+
313
+ if (!up) {
314
+ data.push({
315
+ from: 'sunrpc',
316
+ protocol: protocols[0],
317
+ port,
318
+ info: 'No RPC services found',
319
+ banner: null,
320
+ status: 'filtered',
321
+ service: 'sunrpc',
322
+
323
+ // legacy fields
324
+ probe_protocol: protocols[0],
325
+ probe_port: port,
326
+ probe_info: 'No RPC services found',
327
+ response_banner: null
328
+ });
329
+ }
330
+
331
+ return {
332
+ up,
333
+ program,
334
+ version,
335
+ type: 'sunrpc',
336
+ data
337
+ };
338
+ }
339
+ };
@@ -0,0 +1,314 @@
1
+ // plugins/syn_scanner.mjs
2
+ // TCP SYN Scanner — optional Nmap wrapper for SYN (-sS) scanning.
3
+ // Gated by ENABLE_SYN_SCAN env var (default: false).
4
+ // Requires nmap to be installed; falls back gracefully when missing.
5
+
6
+ import { execFile } from "node:child_process";
7
+ import { promisify } from "node:util";
8
+
9
+ const execFileP = promisify(execFile);
10
+
11
+ /* ------------------------------ helpers ------------------------------ */
12
+
13
+ const HOST_RE = /^[a-zA-Z0-9._:-]+$/;
14
+
15
+ function isValidHost(host) {
16
+ if (!host || typeof host !== "string") return false;
17
+ if (host.length > 253) return false;
18
+ return HOST_RE.test(host);
19
+ }
20
+
21
+ /**
22
+ * Parse Nmap XML output (from -oX -) using regex.
23
+ * Returns { hosts: [{ ip, status, ports: [{ port, protocol, state, service, version }], os }] }
24
+ */
25
+ export function parseNmapXml(xml) {
26
+ const result = { hosts: [] };
27
+ if (!xml || typeof xml !== "string") return result;
28
+
29
+ // Match each <host>...</host> block
30
+ const hostBlocks = xml.match(/<host[\s>][\s\S]*?<\/host>/gi);
31
+ if (!hostBlocks) return result;
32
+
33
+ for (const block of hostBlocks) {
34
+ const host = { ip: null, status: null, ports: [], os: null };
35
+
36
+ // Extract address
37
+ const addrMatch = block.match(/<address\s+addr="([^"]+)"/i);
38
+ if (addrMatch) host.ip = addrMatch[1];
39
+
40
+ // Extract status
41
+ const statusMatch = block.match(/<status\s+state="([^"]+)"/i);
42
+ if (statusMatch) host.status = statusMatch[1];
43
+
44
+ // Extract ports
45
+ const portMatches = block.match(/<port[\s>][\s\S]*?<\/port>/gi);
46
+ if (portMatches) {
47
+ for (const portBlock of portMatches) {
48
+ const portInfo = {};
49
+
50
+ const protoMatch = portBlock.match(/protocol="([^"]+)"/i);
51
+ portInfo.protocol = protoMatch ? protoMatch[1] : "tcp";
52
+
53
+ const portIdMatch = portBlock.match(/portid="([^"]+)"/i);
54
+ portInfo.port = portIdMatch ? Number(portIdMatch[1]) : 0;
55
+
56
+ const stateMatch = portBlock.match(/<state\s+state="([^"]+)"/i);
57
+ portInfo.state = stateMatch ? stateMatch[1] : "unknown";
58
+
59
+ const svcNameMatch = portBlock.match(/<service\s+name="([^"]+)"/i);
60
+ portInfo.service = svcNameMatch ? svcNameMatch[1] : null;
61
+
62
+ // Extract product and version from service tag attributes
63
+ const productMatch = portBlock.match(/product="([^"]+)"/i);
64
+ const versionMatch = portBlock.match(/version="([^"]+)"/i);
65
+ portInfo.version = null;
66
+ if (productMatch && versionMatch) {
67
+ portInfo.version = `${productMatch[1]} ${versionMatch[1]}`;
68
+ } else if (productMatch) {
69
+ portInfo.version = productMatch[1];
70
+ } else if (versionMatch) {
71
+ portInfo.version = versionMatch[1];
72
+ }
73
+
74
+ host.ports.push(portInfo);
75
+ }
76
+ }
77
+
78
+ // Extract OS match
79
+ const osMatch = block.match(/<osmatch\s+name="([^"]+)"/i);
80
+ if (osMatch) host.os = osMatch[1];
81
+
82
+ result.hosts.push(host);
83
+ }
84
+
85
+ return result;
86
+ }
87
+
88
+ /**
89
+ * Build the nmap argument list.
90
+ */
91
+ function buildNmapArgs(host) {
92
+ const args = ["-sS", "-Pn", "-T4", "--min-rate", "100", "-oX", "-"];
93
+
94
+ const portRange = process.env.SYN_SCAN_PORTS;
95
+ const PORT_RANGE_RE = /^[\dT:U:,\-\s]+$/;
96
+ if (portRange && typeof portRange === "string" && portRange.trim() && PORT_RANGE_RE.test(portRange.trim())) {
97
+ args.push("-p", portRange.trim());
98
+ }
99
+
100
+ args.push(host);
101
+ return args;
102
+ }
103
+
104
+ /**
105
+ * Check if nmap is available on the system.
106
+ */
107
+ async function isNmapAvailable() {
108
+ try {
109
+ // Use 'command -v' on Unix-like, 'where' on Windows
110
+ const cmd = process.platform === "win32" ? "where" : "command";
111
+ const cmdArgs = process.platform === "win32" ? ["nmap"] : ["-v", "nmap"];
112
+ await execFileP(cmd, cmdArgs, { timeout: 5000 });
113
+ return true;
114
+ } catch {
115
+ return false;
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Check if SYN scanning is enabled via env var.
121
+ */
122
+ function isSynScanEnabled() {
123
+ const v = String(process.env.ENABLE_SYN_SCAN || "").toLowerCase();
124
+ return v === "1" || v === "true" || v === "yes" || v === "on";
125
+ }
126
+
127
+ /* ------------------------------ plugin ------------------------------ */
128
+
129
+ export default {
130
+ id: "024",
131
+ name: "TCP SYN Scanner (Nmap)",
132
+ description:
133
+ "SYN scan via Nmap wrapper. Requires nmap installed and ENABLE_SYN_SCAN=1. Falls back gracefully when unavailable.",
134
+ priority: 12,
135
+ protocols: ["tcp"],
136
+ ports: [],
137
+ requirements: {},
138
+
139
+ async run(host, _port = 0, opts = {}) {
140
+ // Gate: must be explicitly enabled
141
+ if (!isSynScanEnabled()) {
142
+ return {
143
+ up: false,
144
+ program: "Unknown",
145
+ version: "Unknown",
146
+ os: null,
147
+ type: "syn-scan",
148
+ tcpOpen: [],
149
+ data: [{ probe_info: "SYN scan disabled (set ENABLE_SYN_SCAN=1 to enable)" }],
150
+ };
151
+ }
152
+
153
+ // Validate host to prevent command injection
154
+ if (!isValidHost(host)) {
155
+ return {
156
+ up: false,
157
+ program: "Unknown",
158
+ version: "Unknown",
159
+ os: null,
160
+ type: "syn-scan",
161
+ tcpOpen: [],
162
+ data: [{ probe_info: `Invalid host: ${String(host).slice(0, 50)}` }],
163
+ };
164
+ }
165
+
166
+ // Check nmap availability
167
+ const nmapFound = await isNmapAvailable();
168
+ if (!nmapFound) {
169
+ return {
170
+ up: false,
171
+ program: "Unknown",
172
+ version: "Unknown",
173
+ os: null,
174
+ type: "syn-scan",
175
+ tcpOpen: [],
176
+ data: [{ probe_info: "nmap not found, falling back to TCP connect scan" }],
177
+ };
178
+ }
179
+
180
+ // Run nmap SYN scan
181
+ const timeoutMs = Number(process.env.SYN_SCAN_TIMEOUT) || 30000;
182
+ const args = buildNmapArgs(host);
183
+
184
+ let stdout, stderr;
185
+ try {
186
+ const result = await execFileP("nmap", args, {
187
+ timeout: timeoutMs,
188
+ maxBuffer: 10 * 1024 * 1024,
189
+ });
190
+ stdout = result.stdout || "";
191
+ stderr = result.stderr || "";
192
+ } catch (err) {
193
+ const msg = String(err?.stderr || err?.message || err);
194
+
195
+ // Detect permission issues (SYN scan requires root/sudo)
196
+ if (/permission|operation not permitted|requires root|raw socket|not authorized/i.test(msg)) {
197
+ return {
198
+ up: false,
199
+ program: "Unknown",
200
+ version: "Unknown",
201
+ os: null,
202
+ type: "syn-scan",
203
+ tcpOpen: [],
204
+ data: [
205
+ {
206
+ probe_info:
207
+ "SYN scan requires root/sudo privileges. Run with sudo or use the TCP connect scanner instead.",
208
+ },
209
+ ],
210
+ };
211
+ }
212
+
213
+ return {
214
+ up: false,
215
+ program: "Unknown",
216
+ version: "Unknown",
217
+ os: null,
218
+ type: "syn-scan",
219
+ tcpOpen: [],
220
+ data: [{ probe_info: `nmap error: ${msg.slice(0, 200)}` }],
221
+ };
222
+ }
223
+
224
+ // Parse XML output
225
+ const parsed = parseNmapXml(stdout);
226
+
227
+ const data = [];
228
+ const tcpOpen = [];
229
+ let hostUp = false;
230
+ let detectedOs = null;
231
+
232
+ for (const h of parsed.hosts) {
233
+ if (h.status === "up") hostUp = true;
234
+ if (h.os) detectedOs = h.os;
235
+
236
+ for (const p of h.ports) {
237
+ const row = {
238
+ probe_protocol: p.protocol || "tcp",
239
+ probe_port: p.port,
240
+ status: p.state,
241
+ probe_info: `SYN scan: ${p.state}${p.service ? ` (${p.service})` : ""}`,
242
+ response_banner: p.version || null,
243
+ service: p.service || null,
244
+ };
245
+ data.push(row);
246
+
247
+ if (p.state === "open" && (p.protocol || "tcp") === "tcp") {
248
+ tcpOpen.push(p.port);
249
+ }
250
+ }
251
+ }
252
+
253
+ // Update context if provided
254
+ const ctx = opts?.context;
255
+ if (ctx) {
256
+ if (hostUp) ctx.hostUp = true;
257
+ if (ctx.tcpOpen && typeof ctx.tcpOpen.add === "function") {
258
+ for (const p of tcpOpen) ctx.tcpOpen.add(p);
259
+ }
260
+ if (detectedOs && !ctx.os) {
261
+ ctx.os = detectedOs;
262
+ ctx.guessedOs = detectedOs;
263
+ }
264
+ }
265
+
266
+ return {
267
+ up: hostUp || tcpOpen.length > 0,
268
+ program: "nmap",
269
+ version: "Unknown",
270
+ os: detectedOs,
271
+ type: "syn-scan",
272
+ tcpOpen,
273
+ data,
274
+ };
275
+ },
276
+ };
277
+
278
+ /* ------------------------------ conclude adapter ------------------------------ */
279
+
280
+ import { statusFrom } from "../utils/conclusion_utils.mjs";
281
+
282
+ export async function conclude({ host, result }) {
283
+ const rows = Array.isArray(result?.data) ? result.data : [];
284
+ const items = [];
285
+
286
+ for (const r of rows) {
287
+ const proto = r?.probe_protocol || "tcp";
288
+ const port = Number(r?.probe_port ?? 0);
289
+ if (!port) continue;
290
+
291
+ const state = r?.status || "unknown";
292
+ let status;
293
+ if (state === "open") status = "open";
294
+ else if (state === "closed") status = "closed";
295
+ else if (state === "filtered") status = "filtered";
296
+ else status = statusFrom({ info: r?.probe_info, banner: r?.response_banner, fallbackUp: result?.up });
297
+
298
+ items.push({
299
+ port,
300
+ protocol: proto,
301
+ service: r?.service || "unknown",
302
+ program: result?.program || "nmap",
303
+ version: r?.response_banner || "Unknown",
304
+ status,
305
+ info: r?.probe_info || null,
306
+ banner: r?.response_banner || null,
307
+ source: "syn_scanner",
308
+ evidence: rows,
309
+ authoritative: false,
310
+ });
311
+ }
312
+
313
+ return items;
314
+ }