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,522 @@
|
|
|
1
|
+
// plugins/mdns_scanner.mjs
|
|
2
|
+
// MDNS Scanner — discovers mDNS/Bonjour services and RETURNS ONLY records
|
|
3
|
+
// that are relevant to the scanned host by default.
|
|
4
|
+
// Relevance = (A/AAAA includes target host IP) OR (TXT contains a MAC that
|
|
5
|
+
// matches the target MAC). Set MDNS_INCLUDE_NON_MATCHED=1 to keep all rows.
|
|
6
|
+
|
|
7
|
+
import { isPrivateLike } from '../utils/net_validation.mjs';
|
|
8
|
+
|
|
9
|
+
const DEBUG = /^(1|true|yes|on)$/i.test(String(process.env.DEBUG_MODE || process.env.MDNS_DEBUG || ""));
|
|
10
|
+
function dlog(...a) { if (DEBUG) console.log("[mdns-scanner]", ...a); }
|
|
11
|
+
|
|
12
|
+
function ipMatches(target, addresses = []) {
|
|
13
|
+
const t = String(target || "").trim();
|
|
14
|
+
return addresses.some(a => String(a).trim() === t);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// --- Robust extraction for JSON, key=value, or inline arrays ---
|
|
18
|
+
function extractAddressesFromBanner(banner) {
|
|
19
|
+
if (!banner) return [];
|
|
20
|
+
const s = String(banner);
|
|
21
|
+
|
|
22
|
+
// Try JSON first
|
|
23
|
+
try {
|
|
24
|
+
const obj = JSON.parse(s);
|
|
25
|
+
if (Array.isArray(obj?.addresses)) return obj.addresses.map(String);
|
|
26
|
+
if (Array.isArray(obj?.addrs)) return obj.addrs.map(String);
|
|
27
|
+
} catch {}
|
|
28
|
+
|
|
29
|
+
// Try "addresses=[...]" JSON-like fragment
|
|
30
|
+
const mj = s.match(/"addresses"\s*:\s*\[([^\]]*)\]/i);
|
|
31
|
+
if (mj) {
|
|
32
|
+
return mj[1]
|
|
33
|
+
.split(",")
|
|
34
|
+
.map(x => x.replace(/["'\s]/g, ""))
|
|
35
|
+
.filter(Boolean);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Try key=value (legacy)
|
|
39
|
+
const mkv = s.match(/addresses\s*=\s*([^\s;]+)/i);
|
|
40
|
+
if (mkv) {
|
|
41
|
+
return mkv[1]
|
|
42
|
+
.split(/[,\s]+/)
|
|
43
|
+
.map(x => x.trim())
|
|
44
|
+
.filter(Boolean);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizeMac(s) {
|
|
51
|
+
if (!s) return null;
|
|
52
|
+
const hex = String(s).trim().toUpperCase();
|
|
53
|
+
const m = hex.match(/^([0-9A-F]{2}[:\-]){5}[0-9A-F]{2}$/) || hex.match(/^[0-9A-F]{12}$/);
|
|
54
|
+
if (!m) return null;
|
|
55
|
+
const flat = hex.replace(/[^0-9A-F]/g, "");
|
|
56
|
+
return flat.length === 12 ? flat : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function anyMacsFromObject(obj) {
|
|
60
|
+
// Extract all MAC-looking values from a TXT object (deviceid, rpBA, bs, etc.)
|
|
61
|
+
const out = [];
|
|
62
|
+
if (!obj || typeof obj !== "object") return out;
|
|
63
|
+
for (const v of Object.values(obj)) {
|
|
64
|
+
const mac = normalizeMac(v);
|
|
65
|
+
if (mac) out.push(mac);
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function macMatchesTarget(txtObj, targetMacNorm) {
|
|
71
|
+
if (!targetMacNorm) return false;
|
|
72
|
+
const found = anyMacsFromObject(txtObj);
|
|
73
|
+
return found.some(m => m === targetMacNorm);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function compactTxt(obj) {
|
|
77
|
+
if (!obj || typeof obj !== 'object') return { banner: null, obj: null };
|
|
78
|
+
const keep = {};
|
|
79
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
80
|
+
if (k.length <= 24 && String(v).length <= 256) keep[k] = v;
|
|
81
|
+
}
|
|
82
|
+
let banner = null;
|
|
83
|
+
try { banner = JSON.stringify(keep); } catch {}
|
|
84
|
+
return { banner, obj: keep };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function unescapeFullname(s) {
|
|
88
|
+
// Convert DNS-SD escaped spaces \032 → ' '
|
|
89
|
+
return String(s || "").replace(/\\032/g, " ");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Deduplicate and merge rows based on probe_info and response_banner
|
|
93
|
+
function deduplicateAndMergeRows(rows) {
|
|
94
|
+
const seen = new Map();
|
|
95
|
+
const servicePairs = [
|
|
96
|
+
['ipp', 'ipps'],
|
|
97
|
+
['uscan', 'uscans']
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
// Helper to extract service type from probe_info
|
|
101
|
+
function getServiceType(probeInfo) {
|
|
102
|
+
const match = probeInfo.match(/^[^—]+—\s*([^.\s]+)\./);
|
|
103
|
+
return match ? match[1] : '';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Helper to merge service types in probe_info
|
|
107
|
+
function mergeProbeInfo(info1, info2, serviceType1, serviceType2) {
|
|
108
|
+
const prefix = info1.match(/^[^—]+—\s*/)[0];
|
|
109
|
+
const rest = info1.replace(/^[^—]+—\s*[^.\s]+\./, '');
|
|
110
|
+
return `${prefix}${serviceType1}/${serviceType2}.${rest}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (const row of rows) {
|
|
114
|
+
const signature = `${row.probe_info}|${row.response_banner || ''}`;
|
|
115
|
+
const serviceType = getServiceType(row.probe_info);
|
|
116
|
+
|
|
117
|
+
// Check if this row can be merged with an existing one
|
|
118
|
+
let merged = false;
|
|
119
|
+
if (row.response_banner) {
|
|
120
|
+
for (const [key, existing] of seen.entries()) {
|
|
121
|
+
if (existing.response_banner === row.response_banner) {
|
|
122
|
+
const existingServiceType = getServiceType(existing.probe_info);
|
|
123
|
+
for (const [type1, type2] of servicePairs) {
|
|
124
|
+
if (
|
|
125
|
+
(serviceType === type1 && existingServiceType === type2) ||
|
|
126
|
+
(serviceType === type2 && existingServiceType === type1)
|
|
127
|
+
) {
|
|
128
|
+
// Merge the service types
|
|
129
|
+
seen.set(key, {
|
|
130
|
+
...existing,
|
|
131
|
+
probe_info: mergeProbeInfo(existing.probe_info, row.probe_info, existingServiceType, serviceType),
|
|
132
|
+
probe_port: Math.min(existing.probe_port, row.probe_port) // Use the lower port if they differ
|
|
133
|
+
});
|
|
134
|
+
merged = true;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (merged) break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// If not merged and not seen, add as new
|
|
144
|
+
if (!merged && !seen.has(signature)) {
|
|
145
|
+
seen.set(signature, row);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return Array.from(seen.values());
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// --------------------------- node-mdns path ---------------------------
|
|
153
|
+
async function runWithNodeMdns(targetHost, timeoutMs, opts) {
|
|
154
|
+
const mdnsMod = await import('mdns');
|
|
155
|
+
const mdns = mdnsMod.default || mdns;
|
|
156
|
+
|
|
157
|
+
const sequence = [
|
|
158
|
+
mdns.rst.DNSServiceResolve(),
|
|
159
|
+
mdns.rst.DNSServiceGetAddrInfo({ families: [4, 6] }),
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
const rows = [];
|
|
163
|
+
let matched = false;
|
|
164
|
+
const stopFns = [];
|
|
165
|
+
const includeNonMatched = /^(1|true|yes|on)$/i.test(String(process.env.MDNS_INCLUDE_NON_MATCHED || ""));
|
|
166
|
+
const targetMacNorm =
|
|
167
|
+
normalizeMac(opts?.targetMac) ||
|
|
168
|
+
normalizeMac(opts?.arpMac) ||
|
|
169
|
+
normalizeMac(opts?.context?.arpMac);
|
|
170
|
+
|
|
171
|
+
const browserAllTypes = mdns.browseThemAll();
|
|
172
|
+
stopFns.push(() => { try { browserAllTypes.stop(); } catch {} });
|
|
173
|
+
|
|
174
|
+
browserAllTypes.on('serviceUp', serviceType => {
|
|
175
|
+
try {
|
|
176
|
+
const t = serviceType?.type;
|
|
177
|
+
if (!t) return;
|
|
178
|
+
const specificBrowser = mdns.createBrowser(t, { resolverSequence: sequence });
|
|
179
|
+
stopFns.push(() => { try { specificBrowser.stop(); } catch {} });
|
|
180
|
+
|
|
181
|
+
specificBrowser.on('serviceUp', service => {
|
|
182
|
+
const addresses = Array.isArray(service.addresses) ? service.addresses.slice() : [];
|
|
183
|
+
const ipHit = ipMatches(targetHost, addresses);
|
|
184
|
+
|
|
185
|
+
// Build TXT (keep an object for MAC matching + string banner for display)
|
|
186
|
+
const { obj: txtObj } = compactTxt(service.txtRecord || {});
|
|
187
|
+
const macHit = macMatchesTarget(txtObj, targetMacNorm);
|
|
188
|
+
|
|
189
|
+
const keepRow = ipHit || macHit || includeNonMatched;
|
|
190
|
+
|
|
191
|
+
const infoParts = [];
|
|
192
|
+
infoParts.push(`${service.type?.name || "unknown"}.${service.type?.protocol || "tcp"}`);
|
|
193
|
+
if (service.name) infoParts.push(`name="${service.name}"`);
|
|
194
|
+
const model = service.txtRecord?.model || service.txtRecord?.mdl || service.txtRecord?.ty;
|
|
195
|
+
if (model) infoParts.push(`model="${model}"`);
|
|
196
|
+
if (service.host) infoParts.push(`host=${service.host}`);
|
|
197
|
+
if (service.fullname) infoParts.push(`fullname="${unescapeFullname(service.fullname)}"`);
|
|
198
|
+
|
|
199
|
+
if (keepRow) {
|
|
200
|
+
rows.push({
|
|
201
|
+
probe_protocol: "mdns",
|
|
202
|
+
probe_port: Number.isFinite(service.port) ? service.port : 0,
|
|
203
|
+
probe_info: ((ipHit || macHit) ? "Matched host — " : "Discovered — ") + infoParts.join(" "),
|
|
204
|
+
response_banner: JSON.stringify({
|
|
205
|
+
addresses: Array.isArray(service.addresses) ? service.addresses : [],
|
|
206
|
+
txt: txtObj
|
|
207
|
+
})
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (ipHit || macHit) matched = true;
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
specificBrowser.on('serviceDown', () => {});
|
|
215
|
+
specificBrowser.start();
|
|
216
|
+
} catch (e) {
|
|
217
|
+
dlog("node-mdns serviceType handler error:", e?.message || e);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
browserAllTypes.start();
|
|
222
|
+
await new Promise(res => setTimeout(res, timeoutMs));
|
|
223
|
+
for (const fn of stopFns) { try { fn(); } catch {} }
|
|
224
|
+
return { rows: deduplicateAndMergeRows(rows), matched };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ----------------------- multicast-dns fallback ----------------------
|
|
228
|
+
async function runWithMulticastDns(targetHost, timeoutMs, opts) {
|
|
229
|
+
let mdns;
|
|
230
|
+
if (process.env.MDNS_TEST_FAKE && globalThis.__mdnsFakeFactory) {
|
|
231
|
+
mdns = globalThis.__mdnsFakeFactory();
|
|
232
|
+
} else {
|
|
233
|
+
const { default: MDNS } = await import('multicast-dns');
|
|
234
|
+
mdns = MDNS();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const rows = [];
|
|
238
|
+
let matched = false;
|
|
239
|
+
const types = new Set();
|
|
240
|
+
const includeNonMatched = /^(1|true|yes|on)$/i.test(String(process.env.MDNS_INCLUDE_NON_MATCHED || ""));
|
|
241
|
+
const targetMacNorm =
|
|
242
|
+
normalizeMac(opts?.targetMac) ||
|
|
243
|
+
normalizeMac(opts?.arpMac) ||
|
|
244
|
+
normalizeMac(opts?.context?.arpMac);
|
|
245
|
+
|
|
246
|
+
function recordDirectAHits(pkt) {
|
|
247
|
+
const aRecords = []
|
|
248
|
+
.concat(pkt?.answers || [])
|
|
249
|
+
.concat(pkt?.additionals || [])
|
|
250
|
+
.filter(x => x && (x.type === 'A' || x.type === 'AAAA') && x.data);
|
|
251
|
+
|
|
252
|
+
const addrs = aRecords.map(x => x.data);
|
|
253
|
+
if (ipMatches(targetHost, addrs)) {
|
|
254
|
+
matched = true;
|
|
255
|
+
rows.push({
|
|
256
|
+
probe_protocol: "mdns",
|
|
257
|
+
probe_port: 0,
|
|
258
|
+
probe_info: "Matched host — direct A/AAAA hit",
|
|
259
|
+
response_banner: JSON.stringify({ addresses: addrs })
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function parseTxtFor(serviceFqdn, res) {
|
|
265
|
+
const obj = {};
|
|
266
|
+
for (const t of (res.additionals || [])) {
|
|
267
|
+
if (t.type === 'TXT' && t.name === serviceFqdn) {
|
|
268
|
+
const frags = Array.isArray(t.data) ? t.data : [t.data];
|
|
269
|
+
for (const f of frags) {
|
|
270
|
+
const s = Buffer.isBuffer(f) ? f.toString('utf8') : String(f || '');
|
|
271
|
+
const m = s.match(/^([^=]+)=(.*)$/);
|
|
272
|
+
if (m) obj[m[1]] = m[2];
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return compactTxt(obj); // {banner, obj}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function onResponse(res) {
|
|
280
|
+
try {
|
|
281
|
+
// 1) Fast path: direct A/AAAA matches
|
|
282
|
+
recordDirectAHits(res);
|
|
283
|
+
|
|
284
|
+
// 2) Learn service types from the DNS-SD registry
|
|
285
|
+
for (const ans of (res?.answers || [])) {
|
|
286
|
+
if (ans.type === 'PTR' && ans.name === '_services._dns-sd._udp.local') {
|
|
287
|
+
if (typeof ans.data === 'string') types.add(ans.data);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// 3) Parse SRV (+ TXT + A/AAAA) service instances
|
|
292
|
+
for (const ans of (res?.answers || [])) {
|
|
293
|
+
if (ans.type === 'SRV' && /_tcp\.local\.?$/.test(ans.name)) {
|
|
294
|
+
const serviceFqdn = ans.name;
|
|
295
|
+
const port = ans.data?.port || 0;
|
|
296
|
+
const targetHostFqdn = ans.data?.target;
|
|
297
|
+
|
|
298
|
+
// gather addresses for the SRV target
|
|
299
|
+
const addrs = [];
|
|
300
|
+
for (const a of (res.additionals || [])) {
|
|
301
|
+
if ((a.type === 'A' || a.type === 'AAAA') && a.name === targetHostFqdn && a.data) {
|
|
302
|
+
addrs.push(a.data);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
for (const a of (res.answers || [])) {
|
|
306
|
+
if ((a.type === 'A' || a.type === 'AAAA') && a.name === targetHostFqdn && a.data) {
|
|
307
|
+
addrs.push(a.data);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const { obj: txtObj } = parseTxtFor(serviceFqdn, res);
|
|
312
|
+
const ipHit = ipMatches(targetHost, addrs);
|
|
313
|
+
const macHit = macMatchesTarget(txtObj, targetMacNorm);
|
|
314
|
+
const keepRow = ipHit || macHit || includeNonMatched;
|
|
315
|
+
|
|
316
|
+
const typeMatch = serviceFqdn.match(/(_[^.]+)\.(_tcp|_udp)\.local\.?$/);
|
|
317
|
+
const typeStr = typeMatch ? `${typeMatch[1].slice(1)}.${typeMatch[2].slice(1)}` : 'unknown.tcp';
|
|
318
|
+
|
|
319
|
+
const nameField = serviceFqdn.replace(/\._(tcp|udp)\.local\.?$/,'');
|
|
320
|
+
if (keepRow) {
|
|
321
|
+
rows.push({
|
|
322
|
+
probe_protocol: "mdns",
|
|
323
|
+
probe_port: Number.isFinite(port) ? port : 0,
|
|
324
|
+
probe_info: ((ipHit || macHit) ? "Matched host — " : "Discovered — ") +
|
|
325
|
+
`${typeStr} name="${unescapeFullname(nameField)}" host=${targetHostFqdn}`,
|
|
326
|
+
response_banner: JSON.stringify({
|
|
327
|
+
addresses: addrs,
|
|
328
|
+
txt: txtObj,
|
|
329
|
+
fullname: unescapeFullname(serviceFqdn)
|
|
330
|
+
})
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
if (ipHit || macHit) matched = true;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
} catch (e) {
|
|
337
|
+
dlog("multicast-dns parse error:", e?.message || e);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
mdns.on('response', onResponse);
|
|
342
|
+
|
|
343
|
+
// Ask for the list of service types
|
|
344
|
+
mdns.query({ questions: [{ name: '_services._dns-sd._udp.local', type: 'PTR' }] });
|
|
345
|
+
|
|
346
|
+
const interval = setInterval(() => {
|
|
347
|
+
try {
|
|
348
|
+
for (const t of types) mdns.query({ questions: [{ name: t, type: 'PTR' }] });
|
|
349
|
+
} catch {}
|
|
350
|
+
}, 700);
|
|
351
|
+
|
|
352
|
+
await new Promise(res => setTimeout(res, timeoutMs));
|
|
353
|
+
|
|
354
|
+
clearInterval(interval);
|
|
355
|
+
try { mdns.removeListener('response', onResponse); } catch {}
|
|
356
|
+
try { mdns.destroy?.(); } catch {}
|
|
357
|
+
|
|
358
|
+
return { rows: deduplicateAndMergeRows(rows), matched };
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// --------------------------- TR-069 Probe ---------------------------
|
|
362
|
+
async function probeTr069(host, timeoutMs = 5000) {
|
|
363
|
+
const net = await import('net');
|
|
364
|
+
return new Promise((resolve) => {
|
|
365
|
+
const socket = net.createConnection(7547, host, () => {
|
|
366
|
+
dlog("TR-069 connection established");
|
|
367
|
+
const request = `GET / HTTP/1.1\r\nHost: ${host}:7547\r\nConnection: close\r\n\r\n`;
|
|
368
|
+
socket.write(request);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
let response = '';
|
|
372
|
+
const MAX_TR069_RESPONSE = 32768;
|
|
373
|
+
socket.setTimeout(timeoutMs);
|
|
374
|
+
socket.on('data', (data) => {
|
|
375
|
+
if (response.length > MAX_TR069_RESPONSE) { socket.destroy(); return; }
|
|
376
|
+
response += data.toString();
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
socket.on('end', () => {
|
|
380
|
+
dlog("TR-069 response received");
|
|
381
|
+
resolve(response);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
socket.on('timeout', () => {
|
|
385
|
+
dlog("TR-069 probe timeout");
|
|
386
|
+
socket.destroy();
|
|
387
|
+
resolve(null);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
socket.on('error', (e) => {
|
|
391
|
+
dlog("TR-069 probe error:", e.message);
|
|
392
|
+
resolve(null);
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ----------------------------- Plugin -----------------------------
|
|
398
|
+
export default {
|
|
399
|
+
id: "027",
|
|
400
|
+
name: "MDNS Scanner",
|
|
401
|
+
description: "Discovers Bonjour/mDNS services and (by default) returns only instances that advertise the target host address or MAC.",
|
|
402
|
+
// Runs after Ping/DNS so OS Detector can consume its rows later.
|
|
403
|
+
priority: 345,
|
|
404
|
+
requirements: {},
|
|
405
|
+
protocols: ["mdns"],
|
|
406
|
+
ports: [],
|
|
407
|
+
runStrategy: "single",
|
|
408
|
+
|
|
409
|
+
async run(host, _port = 0, opts = {}) {
|
|
410
|
+
const timeoutMs = Number(opts.timeoutMs ?? process.env.NSA_MDNS_TIMEOUT_MS ?? 7000);
|
|
411
|
+
const data = [];
|
|
412
|
+
|
|
413
|
+
if (!isPrivateLike(host)) {
|
|
414
|
+
data.push({
|
|
415
|
+
probe_protocol: "mdns",
|
|
416
|
+
probe_port: 0,
|
|
417
|
+
probe_info: "Non-local target — mDNS not attempted",
|
|
418
|
+
response_banner: null
|
|
419
|
+
});
|
|
420
|
+
return {
|
|
421
|
+
up: false,
|
|
422
|
+
program: "mDNS/Bonjour",
|
|
423
|
+
version: "Unknown",
|
|
424
|
+
os: null,
|
|
425
|
+
type: "mdns",
|
|
426
|
+
data
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
let rows = [];
|
|
431
|
+
let anyMatched = false;
|
|
432
|
+
|
|
433
|
+
// allow tests/users to force multicast-dns path
|
|
434
|
+
const forceFallback = /^(1|true|yes|on)$/i.test(String(process.env.MDNS_FORCE_FALLBACK || ""));
|
|
435
|
+
|
|
436
|
+
try {
|
|
437
|
+
if (!forceFallback) {
|
|
438
|
+
dlog("Trying node-mdns strategy…");
|
|
439
|
+
const out = await runWithNodeMdns(host, timeoutMs, opts);
|
|
440
|
+
rows = out.rows;
|
|
441
|
+
anyMatched = out.matched;
|
|
442
|
+
} else {
|
|
443
|
+
throw new Error("Forced fallback");
|
|
444
|
+
}
|
|
445
|
+
} catch (e) {
|
|
446
|
+
dlog("node-mdns not available or forced/failure:", e?.message || e);
|
|
447
|
+
try {
|
|
448
|
+
dlog("Falling back to multicast-dns strategy…");
|
|
449
|
+
const out2 = await runWithMulticastDns(host, timeoutMs, opts);
|
|
450
|
+
rows = out2.rows;
|
|
451
|
+
anyMatched = out2.matched;
|
|
452
|
+
} catch (e2) {
|
|
453
|
+
dlog("multicast-dns failed:", e2?.message || e2);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (rows.length === 0) {
|
|
458
|
+
data.push({
|
|
459
|
+
probe_protocol: "mdns",
|
|
460
|
+
probe_port: 0,
|
|
461
|
+
probe_info: "No mDNS records relevant to target (IP/MAC) observed in timeout window",
|
|
462
|
+
response_banner: null
|
|
463
|
+
});
|
|
464
|
+
} else {
|
|
465
|
+
data.push(...deduplicateAndMergeRows(rows));
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Re-check across aggregated rows and emit the explicit test-visible line
|
|
469
|
+
try {
|
|
470
|
+
const postMatch = data.some(r => {
|
|
471
|
+
const addrs = extractAddressesFromBanner(r?.response_banner);
|
|
472
|
+
return addrs.includes(host);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
if (postMatch) {
|
|
476
|
+
const matchRow = {
|
|
477
|
+
probe_protocol: "mdns",
|
|
478
|
+
probe_port: 5353,
|
|
479
|
+
probe_info: `Matched host IP ${host} via mDNS`,
|
|
480
|
+
response_banner: null
|
|
481
|
+
};
|
|
482
|
+
// Only add the match row if it's not already present
|
|
483
|
+
if (!data.some(r => r.probe_info === matchRow.probe_info && r.response_banner === matchRow.response_banner)) {
|
|
484
|
+
data.push(matchRow);
|
|
485
|
+
}
|
|
486
|
+
anyMatched = true; // reflect in final "up"
|
|
487
|
+
}
|
|
488
|
+
} catch {}
|
|
489
|
+
|
|
490
|
+
// TR-069 Probe if mDNS matched (indicating potential router/device)
|
|
491
|
+
if (anyMatched) {
|
|
492
|
+
const tr069Response = await probeTr069(host, timeoutMs);
|
|
493
|
+
if (tr069Response) {
|
|
494
|
+
// Extract useful info, e.g., Server header for firmware/OS
|
|
495
|
+
const serverMatch = tr069Response.match(/Server: ([^\r\n]+)/i);
|
|
496
|
+
const info = serverMatch ? `TR-069 detected — ${serverMatch[1]}` : "TR-069 detected";
|
|
497
|
+
data.push({
|
|
498
|
+
probe_protocol: "tr069",
|
|
499
|
+
probe_port: 7547,
|
|
500
|
+
probe_info: info,
|
|
501
|
+
response_banner: tr069Response.trim().slice(0, 512) // Limit banner size
|
|
502
|
+
});
|
|
503
|
+
} else {
|
|
504
|
+
data.push({
|
|
505
|
+
probe_protocol: "tr069",
|
|
506
|
+
probe_port: 7547,
|
|
507
|
+
probe_info: "No TR-069 response",
|
|
508
|
+
response_banner: null
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return {
|
|
514
|
+
up: anyMatched,
|
|
515
|
+
program: "mDNS/Bonjour",
|
|
516
|
+
version: "Unknown",
|
|
517
|
+
os: null,
|
|
518
|
+
type: "mdns",
|
|
519
|
+
data
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
};
|