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.
- package/CONTRIBUTING.md +24 -0
- package/LICENSE +21 -0
- package/README.md +584 -0
- package/bin/nsauditor-ai-mcp.mjs +2 -0
- package/bin/nsauditor-ai.mjs +2 -0
- package/cli.mjs +939 -0
- package/config/services.json +304 -0
- package/docs/EULA-nsauditor-ai.md +324 -0
- package/index.mjs +15 -0
- package/mcp_server.mjs +382 -0
- package/package.json +44 -0
- package/plugin_manager.mjs +829 -0
- package/plugins/arp_scanner.mjs +162 -0
- package/plugins/db_scanner.mjs +248 -0
- package/plugins/dns_scanner.mjs +369 -0
- package/plugins/dnssd-scanner.mjs +245 -0
- package/plugins/ftp_banner_check.mjs +247 -0
- package/plugins/host_up_check.mjs +337 -0
- package/plugins/http_probe.mjs +290 -0
- package/plugins/llmnr_scanner.mjs +130 -0
- package/plugins/mdns_scanner.mjs +522 -0
- package/plugins/netbios_scanner.mjs +737 -0
- package/plugins/opensearch_scanner.mjs +276 -0
- package/plugins/os_detector.mjs +436 -0
- package/plugins/ping_checker.mjs +271 -0
- package/plugins/port_scanner.mjs +250 -0
- package/plugins/result_concluder.mjs +274 -0
- package/plugins/snmp_scanner.mjs +278 -0
- package/plugins/ssh_scanner.mjs +421 -0
- package/plugins/sunrpc_scanner.mjs +339 -0
- package/plugins/syn_scanner.mjs +314 -0
- package/plugins/tls_scanner.mjs +225 -0
- package/plugins/upnp_scanner.mjs +441 -0
- package/plugins/webapp_detector.mjs +246 -0
- package/plugins/wsd_scanner.mjs +290 -0
- package/utils/attack_map.mjs +180 -0
- package/utils/capabilities.mjs +53 -0
- package/utils/conclusion_utils.mjs +70 -0
- package/utils/cpe.mjs +74 -0
- package/utils/cve_validator.mjs +64 -0
- package/utils/cvss.mjs +129 -0
- package/utils/delta_reporter.mjs +110 -0
- package/utils/export_csv.mjs +82 -0
- package/utils/finding_queue.mjs +64 -0
- package/utils/finding_schema.mjs +36 -0
- package/utils/host_iterator.mjs +166 -0
- package/utils/license.mjs +29 -0
- package/utils/net_validation.mjs +66 -0
- package/utils/nvd_cache.mjs +77 -0
- package/utils/nvd_client.mjs +130 -0
- package/utils/oui.mjs +107 -0
- package/utils/plugin_discovery.mjs +89 -0
- package/utils/prompts.mjs +143 -0
- package/utils/raw_report_html.mjs +170 -0
- package/utils/redact.mjs +79 -0
- package/utils/report_html.mjs +236 -0
- package/utils/sarif.mjs +225 -0
- package/utils/scan_history.mjs +248 -0
- package/utils/scheduler.mjs +157 -0
- 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
|
+
};
|