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,246 @@
|
|
|
1
|
+
// plugins/webapp_detector.mjs
|
|
2
|
+
// New plugin: Webapp Detector
|
|
3
|
+
// Uses `simple-wappalyzer` to fingerprint web applications present on a host.
|
|
4
|
+
// Tries HTTPS first (port 443), then HTTP (port 80), and can also try custom ports via opts.ports.
|
|
5
|
+
// NOTE: Unlike http_probe, undici/fetch cannot ignore TLS easily per-request, so self-signed
|
|
6
|
+
// HTTPS will usually fail and the plugin will fall back to HTTP.
|
|
7
|
+
//
|
|
8
|
+
// Add to package.json (dependencies):
|
|
9
|
+
// "simple-wappalyzer": "^1.14.0" // or latest
|
|
10
|
+
//
|
|
11
|
+
// Example use (plugin manager):
|
|
12
|
+
// webappDetector.run("192.168.1.1")
|
|
13
|
+
//
|
|
14
|
+
// Result shape example:
|
|
15
|
+
// {
|
|
16
|
+
// up: true,
|
|
17
|
+
// program: "WordPress + Nginx",
|
|
18
|
+
// version: "Unknown",
|
|
19
|
+
// os: null,
|
|
20
|
+
// type: "webapp",
|
|
21
|
+
// data: [
|
|
22
|
+
// {
|
|
23
|
+
// probe_protocol: "https",
|
|
24
|
+
// probe_port: 443,
|
|
25
|
+
// probe_info: "Detected web apps: WordPress, Nginx",
|
|
26
|
+
// response_banner: "200 OK\r\nserver: nginx\r\nx-powered-by: PHP/8.2"
|
|
27
|
+
// }
|
|
28
|
+
// ],
|
|
29
|
+
// apps: [ { name, categories, confidence, version?, slug, ... }, ... ]
|
|
30
|
+
// }
|
|
31
|
+
|
|
32
|
+
import wappalyzer from 'simple-wappalyzer';
|
|
33
|
+
|
|
34
|
+
const DEBUG =
|
|
35
|
+
String(process.env.DEBUG_MODE || '').toLowerCase() === '1' ||
|
|
36
|
+
String(process.env.DEBUG_MODE || '').toLowerCase() === 'true';
|
|
37
|
+
|
|
38
|
+
function log(...args) {
|
|
39
|
+
if (DEBUG) console.log('[webapp-detector]', ...args);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseExtraHeaders() {
|
|
43
|
+
try {
|
|
44
|
+
if (!process.env.HTTP_EXTRA_HEADERS) return {};
|
|
45
|
+
const h = JSON.parse(process.env.HTTP_EXTRA_HEADERS);
|
|
46
|
+
return h && typeof h === 'object' ? h : {};
|
|
47
|
+
} catch {
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function buildBanner(statusCode, headers) {
|
|
53
|
+
const lines = [];
|
|
54
|
+
const statusLine = `${statusCode || 0}`;
|
|
55
|
+
lines.push(statusLine + (headers['status-message'] ? ' ' + headers['status-message'] : ''));
|
|
56
|
+
const pick = ['server', 'x-powered-by', 'www-authenticate', 'content-type', 'location', 'set-cookie'];
|
|
57
|
+
for (const k of pick) {
|
|
58
|
+
const v = headers[k];
|
|
59
|
+
if (!v) continue;
|
|
60
|
+
if (Array.isArray(v)) {
|
|
61
|
+
for (const vv of v) lines.push(`${k}: ${vv}`);
|
|
62
|
+
} else {
|
|
63
|
+
lines.push(`${k}: ${v}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return lines.join('\\r\\n');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function normalizeTarget(target) {
|
|
70
|
+
if (!target) return null;
|
|
71
|
+
if (typeof target === 'string') return target.replace(/^https?:\/\//i, '').split('/')[0];
|
|
72
|
+
return (target.host || target.hostname || target.name || '').replace(/^https?:\/\//i, '').split('/')[0];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function fetchOnce(url, signal) {
|
|
76
|
+
const extra = parseExtraHeaders();
|
|
77
|
+
const headers = {
|
|
78
|
+
'User-Agent': 'Mozilla/5.0 (compatible; NetworkSecurityAuditor/1.18.0; +https://example.invalid)',
|
|
79
|
+
DNT: '1',
|
|
80
|
+
...extra,
|
|
81
|
+
};
|
|
82
|
+
// global fetch (undici) is available in Node >=18
|
|
83
|
+
const res = await fetch(url, { redirect: 'follow', headers, signal });
|
|
84
|
+
const finalUrl = res.url || url;
|
|
85
|
+
const statusCode = res.status;
|
|
86
|
+
const rawHeaders = {};
|
|
87
|
+
res.headers.forEach((v, k) => (rawHeaders[k.toLowerCase()] = v));
|
|
88
|
+
const html = await res.text();
|
|
89
|
+
return { url: finalUrl, statusCode, headers: rawHeaders, html };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function tryDetectAt(url) {
|
|
93
|
+
const ctrl = new AbortController();
|
|
94
|
+
const timeoutMs = Number(process.env.WAPPALYZER_TIMEOUT_MS || 15000);
|
|
95
|
+
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
96
|
+
try {
|
|
97
|
+
log('fetch start —', url);
|
|
98
|
+
const { url: finalUrl, html, statusCode, headers } = await fetchOnce(url, ctrl.signal);
|
|
99
|
+
log('fetch end —', finalUrl, statusCode, `html=${html?.length ?? 0}`);
|
|
100
|
+
const apps = await detectFromHtml(finalUrl, html, statusCode, headers);
|
|
101
|
+
return { ok: true, finalUrl, statusCode, headers, apps };
|
|
102
|
+
} catch (e) {
|
|
103
|
+
log('fetch error —', url, e?.message || e);
|
|
104
|
+
return { ok: false, error: e };
|
|
105
|
+
} finally {
|
|
106
|
+
clearTimeout(t);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Run wappalyzer on provided HTML/headers. */
|
|
111
|
+
async function detectFromHtml(url, html, statusCode, headers) {
|
|
112
|
+
try {
|
|
113
|
+
const result = await wappalyzer({ url, html, statusCode, headers });
|
|
114
|
+
if (Array.isArray(result) && result.length) {
|
|
115
|
+
log('wappalyzer apps=', result.map(a => a.name).join(', '));
|
|
116
|
+
} else {
|
|
117
|
+
log('wappalyzer apps=∅');
|
|
118
|
+
}
|
|
119
|
+
return result || [];
|
|
120
|
+
} catch (e) {
|
|
121
|
+
log('wappalyzer error:', e?.message || e);
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function summarizeApps(apps) {
|
|
127
|
+
if (!Array.isArray(apps) || !apps.length) return { program: null, version: 'Unknown', list: [] };
|
|
128
|
+
// Sort by confidence descending then name
|
|
129
|
+
const sorted = [...apps].sort((a, b) => (Number(b.confidence||0) - Number(a.confidence||0)) || String(a.name).localeCompare(String(b.name)));
|
|
130
|
+
const names = sorted.map(a => a.name).filter(Boolean);
|
|
131
|
+
const program = names.slice(0, 3).join(' + ') || null;
|
|
132
|
+
// If exactly 1 app and it has a version, expose it
|
|
133
|
+
const version = (sorted.length === 1 && sorted[0]?.version) ? String(sorted[0].version) : 'Unknown';
|
|
134
|
+
return { program, version, list: names };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export default {
|
|
138
|
+
id: '010',
|
|
139
|
+
name: 'Webapp Detector',
|
|
140
|
+
description: 'Identifies web applications and frameworks using simple-wappalyzer (tries HTTPS then HTTP).',
|
|
141
|
+
priority: 55, // run near HTTP probe
|
|
142
|
+
requirements: { host: 'up', tcp_open: [80, 443] }, // heuristic gate; still attempts both
|
|
143
|
+
protocols: ['tcp'],
|
|
144
|
+
ports: [80, 443],
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* @param {string} host - target hostname or IP
|
|
148
|
+
* @param {number} port - optional hint (ignored; detection tries 443 then 80 unless opts.ports provided)
|
|
149
|
+
* @param {object} opts - options: { ports?: number[] }
|
|
150
|
+
*/
|
|
151
|
+
async run(host, port = 0, opts = {}) {
|
|
152
|
+
const result = {
|
|
153
|
+
up: false,
|
|
154
|
+
program: null,
|
|
155
|
+
version: 'Unknown',
|
|
156
|
+
os: null,
|
|
157
|
+
type: 'webapp',
|
|
158
|
+
data: [],
|
|
159
|
+
apps: [], // raw simple-wappalyzer results
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
let target = normalizeTarget(host);
|
|
163
|
+
if (!target) return result;
|
|
164
|
+
|
|
165
|
+
// Build candidate URLs
|
|
166
|
+
const set = new Set();
|
|
167
|
+
const addUrl = (proto, p) => {
|
|
168
|
+
const defaultPort = (proto === 'https' ? 443 : 80);
|
|
169
|
+
const portPart = (p && p !== defaultPort) ? `:${p}` : '';
|
|
170
|
+
set.add(`${proto}://${target}${portPart}/`);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// If specific ports given, try both schemes for each; else default to 443 then 80
|
|
174
|
+
const ports = Array.isArray(opts.ports) && opts.ports.length ? opts.ports : [443, 80];
|
|
175
|
+
for (const p of ports) {
|
|
176
|
+
if (p === 443) addUrl('https', 443);
|
|
177
|
+
else if (p === 80) addUrl('http', 80);
|
|
178
|
+
else { addUrl('https', p); addUrl('http', p); }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Try in order added (prefers https:443, then http:80, then customs)
|
|
182
|
+
for (const url of set) {
|
|
183
|
+
const r = await tryDetectAt(url);
|
|
184
|
+
if (r.ok) {
|
|
185
|
+
result.up = true;
|
|
186
|
+
result.apps = r.apps || [];
|
|
187
|
+
const { program, version, list } = summarizeApps(result.apps);
|
|
188
|
+
if (program) result.program = program;
|
|
189
|
+
if (version) result.version = version;
|
|
190
|
+
|
|
191
|
+
// Prepare a concise banner
|
|
192
|
+
const proto = url.startsWith('https:') ? 'https' : 'http';
|
|
193
|
+
const portFromUrl = (() => {
|
|
194
|
+
try {
|
|
195
|
+
const u = new URL(url);
|
|
196
|
+
return Number(u.port || (u.protocol === 'https:' ? 443 : 80));
|
|
197
|
+
} catch { return proto === 'https' ? 443 : 80; }
|
|
198
|
+
})();
|
|
199
|
+
|
|
200
|
+
result.data.push({
|
|
201
|
+
probe_protocol: proto,
|
|
202
|
+
probe_port: portFromUrl,
|
|
203
|
+
probe_info: list.length ? `Detected web apps: ${list.join(', ')}` : `HTTP service detected (status ${r.statusCode})`,
|
|
204
|
+
response_banner: buildBanner(r.statusCode, r.headers),
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Stop after first successful detection
|
|
208
|
+
break;
|
|
209
|
+
} else {
|
|
210
|
+
// Log error row for visibility
|
|
211
|
+
const proto = url.startsWith('https:') ? 'https' : 'http';
|
|
212
|
+
const portFromUrl = (() => {
|
|
213
|
+
try { const u = new URL(url); return Number(u.port || (u.protocol === 'https:' ? 443 : 80)); } catch { return proto === 'https' ? 443 : 80; }
|
|
214
|
+
})();
|
|
215
|
+
result.data.push({
|
|
216
|
+
probe_protocol: proto,
|
|
217
|
+
probe_port: portFromUrl,
|
|
218
|
+
probe_info: `Webapp detect error: ${r.error?.message || String(r.error || 'unknown error')}`,
|
|
219
|
+
response_banner: null,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return result;
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// Concluder adapter: emits detected apps as service records for result fusion.
|
|
229
|
+
export async function conclude({ host, result }) {
|
|
230
|
+
if (!result?.up || !Array.isArray(result.apps) || result.apps.length === 0) return [];
|
|
231
|
+
|
|
232
|
+
// Extract port from the first successful probe record
|
|
233
|
+
const probeRecord = result.data?.find(d => d.probe_port && d.probe_port > 0);
|
|
234
|
+
const port = probeRecord?.probe_port ?? 80;
|
|
235
|
+
const protocol = probeRecord?.probe_protocol ?? 'tcp';
|
|
236
|
+
|
|
237
|
+
// Emit one service record per detected app
|
|
238
|
+
return result.apps.map(app => ({
|
|
239
|
+
protocol,
|
|
240
|
+
port,
|
|
241
|
+
service: app.name ?? 'webapp',
|
|
242
|
+
version: app.version ?? null,
|
|
243
|
+
info: Array.isArray(app.categories) ? app.categories.join(', ') : 'webapp',
|
|
244
|
+
authoritative: false,
|
|
245
|
+
}));
|
|
246
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
// plugins/wsd_scanner.mjs
|
|
2
|
+
// Enhanced WS-Discovery Scanner — discovers WS-Discovery devices in the subnet with comprehensive analysis
|
|
3
|
+
// Filters results to include only devices matching the target host IP by default.
|
|
4
|
+
// Set WSD_INCLUDE_NON_MATCHED=1 to keep all discovered devices.
|
|
5
|
+
|
|
6
|
+
import dgram from 'dgram';
|
|
7
|
+
import { parseString } from 'xml2js';
|
|
8
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
9
|
+
import { isPrivateLike } from '../utils/net_validation.mjs';
|
|
10
|
+
|
|
11
|
+
const DEBUG = /^(1|true|yes|on)$/i.test(String(process.env.DEBUG_MODE || process.env.WSD_DEBUG || ""));
|
|
12
|
+
function dlog(...a) { if (DEBUG) console.log("[wsd-scanner]", ...a); }
|
|
13
|
+
|
|
14
|
+
function ipMatches(target, address) {
|
|
15
|
+
const t = String(target || "").trim();
|
|
16
|
+
const a = String(address || "").trim();
|
|
17
|
+
return t === a;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Discovers WS-Discovery devices by sending a multicast Probe message.
|
|
22
|
+
* @param {string} targetHost The target host IP to filter for
|
|
23
|
+
* @param {number} timeout The time in milliseconds to wait for responses.
|
|
24
|
+
* @returns {Promise<Array<Object>>} A promise that resolves with an array of discovered devices.
|
|
25
|
+
*/
|
|
26
|
+
function discoverWsDiscoveryDevices(targetHost, timeout = 5000) {
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
const client = dgram.createSocket('udp4');
|
|
29
|
+
const devices = [];
|
|
30
|
+
const knownAddresses = new Set();
|
|
31
|
+
const probeMessageId = `urn:uuid:${uuidv4()}`;
|
|
32
|
+
const includeNonMatched = /^(1|true|yes|on)$/i.test(String(process.env.WSD_INCLUDE_NON_MATCHED || ""));
|
|
33
|
+
|
|
34
|
+
// The multicast address and port for WS-Discovery
|
|
35
|
+
const multicastAddress = '239.255.255.250';
|
|
36
|
+
const multicastPort = 3702;
|
|
37
|
+
|
|
38
|
+
// XML-based WS-Discovery Probe message
|
|
39
|
+
const probeMessage = `
|
|
40
|
+
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
|
|
41
|
+
xmlns:a="http://www.w3.org/2005/08/addressing"
|
|
42
|
+
xmlns:d="http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01">
|
|
43
|
+
<s:Header>
|
|
44
|
+
<a:Action>http://docs.oasis-open.org/ws-dd/ns/discovery/2009/01/Probe</a:Action>
|
|
45
|
+
<a:MessageID>${probeMessageId}</a:MessageID>
|
|
46
|
+
<a:To>${multicastAddress}:${multicastPort}</a:To>
|
|
47
|
+
</s:Header>
|
|
48
|
+
<s:Body>
|
|
49
|
+
<d:Probe/>
|
|
50
|
+
</s:Body>
|
|
51
|
+
</s:Envelope>
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
client.on('message', (msg, rinfo) => {
|
|
55
|
+
// Ignore messages that are not responses to our Probe
|
|
56
|
+
if (rinfo.address === client.address().address) return;
|
|
57
|
+
if (knownAddresses.has(rinfo.address)) return;
|
|
58
|
+
|
|
59
|
+
// Filter by target host unless includeNonMatched is set
|
|
60
|
+
const ipHit = ipMatches(targetHost, rinfo.address);
|
|
61
|
+
if (!ipHit && !includeNonMatched) {
|
|
62
|
+
dlog(`Filtering out non-matching device: ${rinfo.address} (target: ${targetHost})`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Parse the XML response from the device
|
|
67
|
+
parseString(msg.toString(), (err, result) => {
|
|
68
|
+
if (err) {
|
|
69
|
+
dlog(`XML parse error from ${rinfo.address}:`, err.message);
|
|
70
|
+
return; // Silently ignore parsing errors
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Ensure this is a valid ProbeMatch message
|
|
74
|
+
try {
|
|
75
|
+
if (result && result['s:Envelope'] && result['s:Envelope']['s:Body'] &&
|
|
76
|
+
result['s:Envelope']['s:Body'][0]['d:ProbeMatches']) {
|
|
77
|
+
const probeMatch = result['s:Envelope']['s:Body'][0]['d:ProbeMatches'][0]['d:ProbeMatch'][0];
|
|
78
|
+
const xaddrs = probeMatch['d:XAddrs'] ? probeMatch['d:XAddrs'][0].split(' ') : [];
|
|
79
|
+
|
|
80
|
+
const endpointRef = probeMatch['a:EndpointReference'] && probeMatch['a:EndpointReference'][0]['a:Address']
|
|
81
|
+
? probeMatch['a:EndpointReference'][0]['a:Address'][0] : 'Unknown';
|
|
82
|
+
|
|
83
|
+
const types = probeMatch['d:Types'] ? probeMatch['d:Types'][0] : 'Unknown';
|
|
84
|
+
const scopes = probeMatch['d:Scopes'] ? probeMatch['d:Scopes'][0] : null;
|
|
85
|
+
const metadataVersion = probeMatch['d:MetadataVersion'] ? probeMatch['d:MetadataVersion'][0] : null;
|
|
86
|
+
|
|
87
|
+
devices.push({
|
|
88
|
+
address: rinfo.address,
|
|
89
|
+
port: rinfo.port,
|
|
90
|
+
xaddrs: xaddrs,
|
|
91
|
+
endpointUuid: endpointRef,
|
|
92
|
+
types: types,
|
|
93
|
+
scopes: scopes,
|
|
94
|
+
metadataVersion: metadataVersion,
|
|
95
|
+
isMatched: ipHit,
|
|
96
|
+
timestamp: new Date().toISOString()
|
|
97
|
+
});
|
|
98
|
+
knownAddresses.add(rinfo.address);
|
|
99
|
+
|
|
100
|
+
dlog(`Discovered WS-Discovery device: ${rinfo.address} (matched: ${ipHit}), types: ${types}`);
|
|
101
|
+
}
|
|
102
|
+
} catch (parseErr) {
|
|
103
|
+
dlog(`Parse error processing response from ${rinfo.address}:`, parseErr.message);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
client.on('error', (err) => {
|
|
109
|
+
dlog("WS-Discovery client error:", err.message);
|
|
110
|
+
client.close();
|
|
111
|
+
reject(err);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
client.bind(() => {
|
|
115
|
+
try {
|
|
116
|
+
client.setBroadcast(true);
|
|
117
|
+
client.setMulticastTTL(128);
|
|
118
|
+
|
|
119
|
+
dlog(`Sending WS-Discovery probe to ${multicastAddress}:${multicastPort}`);
|
|
120
|
+
const buffer = Buffer.from(probeMessage);
|
|
121
|
+
client.send(buffer, multicastPort, multicastAddress, (err) => {
|
|
122
|
+
if (err) {
|
|
123
|
+
dlog("Failed to send WS-Discovery probe:", err.message);
|
|
124
|
+
client.close();
|
|
125
|
+
reject(err);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
} catch (err) {
|
|
129
|
+
dlog("Error setting up WS-Discovery client:", err.message);
|
|
130
|
+
client.close();
|
|
131
|
+
reject(err);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
setTimeout(() => {
|
|
136
|
+
dlog(`WS-Discovery timeout reached, found ${devices.length} devices`);
|
|
137
|
+
client.close();
|
|
138
|
+
resolve(devices);
|
|
139
|
+
}, timeout);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export default {
|
|
144
|
+
id: "016",
|
|
145
|
+
name: "Enhanced WS-Discovery Scanner",
|
|
146
|
+
description: "Discovers WS-Discovery enabled devices using multicast probe messages. Returns only devices matching the target host IP by default.",
|
|
147
|
+
priority: 400,
|
|
148
|
+
requirements: {},
|
|
149
|
+
protocols: ["udp"],
|
|
150
|
+
ports: [3702],
|
|
151
|
+
runStrategy: "single",
|
|
152
|
+
|
|
153
|
+
async run(host, port = 3702, opts = {}) {
|
|
154
|
+
const data = [];
|
|
155
|
+
let up = false;
|
|
156
|
+
let program = "WS-Discovery"; // Always set to WS-Discovery
|
|
157
|
+
let version = "1.1"; // Always set to 1.1
|
|
158
|
+
|
|
159
|
+
const timeout = opts.timeout || 5000;
|
|
160
|
+
|
|
161
|
+
if (!isPrivateLike(host)) {
|
|
162
|
+
data.push({
|
|
163
|
+
probe_protocol: "udp",
|
|
164
|
+
probe_port: port,
|
|
165
|
+
probe_info: "Non-local target — WS-Discovery not attempted (requires local network)",
|
|
166
|
+
response_banner: null
|
|
167
|
+
});
|
|
168
|
+
return {
|
|
169
|
+
up: false,
|
|
170
|
+
program: "WS-Discovery",
|
|
171
|
+
version: "1.1",
|
|
172
|
+
type: "wsdiscovery",
|
|
173
|
+
data
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
dlog(`Starting WS-Discovery scan for host: ${host}`);
|
|
179
|
+
const devices = await discoverWsDiscoveryDevices(host, timeout);
|
|
180
|
+
|
|
181
|
+
const matchedDevices = devices.filter(d => d.isMatched);
|
|
182
|
+
const matched = matchedDevices.length > 0;
|
|
183
|
+
|
|
184
|
+
dlog(`Discovery complete: total=${devices.length}, matched=${matchedDevices.length}`);
|
|
185
|
+
|
|
186
|
+
if (devices.length > 0) {
|
|
187
|
+
up = matched;
|
|
188
|
+
// program and version already set above
|
|
189
|
+
|
|
190
|
+
// Sort devices - matched first
|
|
191
|
+
devices.sort((a, b) => {
|
|
192
|
+
if (a.isMatched && !b.isMatched) return -1;
|
|
193
|
+
if (!a.isMatched && b.isMatched) return 1;
|
|
194
|
+
return 0;
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
devices.forEach((device) => {
|
|
198
|
+
const xaddrsInfo = device.xaddrs.length > 0 ? device.xaddrs.join(', ') : 'No XAddrs';
|
|
199
|
+
|
|
200
|
+
// Build comprehensive info string
|
|
201
|
+
const infoParts = [];
|
|
202
|
+
if (device.isMatched) {
|
|
203
|
+
infoParts.push("Matched host —");
|
|
204
|
+
} else {
|
|
205
|
+
infoParts.push("Discovered —");
|
|
206
|
+
}
|
|
207
|
+
infoParts.push(`types="${device.types}"`);
|
|
208
|
+
infoParts.push(`address=${device.address}`);
|
|
209
|
+
if (device.scopes) {
|
|
210
|
+
infoParts.push(`scopes="${device.scopes}"`);
|
|
211
|
+
}
|
|
212
|
+
if (device.metadataVersion) {
|
|
213
|
+
infoParts.push(`metadataVersion=${device.metadataVersion}`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const bannerObj = {
|
|
217
|
+
address: device.address,
|
|
218
|
+
endpointUuid: device.endpointUuid,
|
|
219
|
+
types: device.types,
|
|
220
|
+
xaddrs: device.xaddrs,
|
|
221
|
+
scopes: device.scopes,
|
|
222
|
+
metadataVersion: device.metadataVersion,
|
|
223
|
+
discoveredAt: device.timestamp,
|
|
224
|
+
isMatched: device.isMatched
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
data.push({
|
|
228
|
+
probe_protocol: 'udp',
|
|
229
|
+
probe_port: device.port,
|
|
230
|
+
probe_info: infoParts.join(' '),
|
|
231
|
+
response_banner: JSON.stringify(bannerObj),
|
|
232
|
+
device_address: device.address,
|
|
233
|
+
device_types: device.types,
|
|
234
|
+
endpoint_uuid: device.endpointUuid,
|
|
235
|
+
xaddrs: device.xaddrs,
|
|
236
|
+
scopes: device.scopes,
|
|
237
|
+
isMatched: device.isMatched
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// Add summary row for matched devices
|
|
242
|
+
if (matched) {
|
|
243
|
+
const uniqueTypes = [...new Set(matchedDevices.map(d => d.types))];
|
|
244
|
+
const summaryRow = {
|
|
245
|
+
probe_protocol: 'udp',
|
|
246
|
+
probe_port: port,
|
|
247
|
+
probe_info: `Host ${host} confirmed via WS-Discovery - ${matchedDevices.length} device(s) found: ${uniqueTypes.join(', ')}`,
|
|
248
|
+
response_banner: JSON.stringify({
|
|
249
|
+
summary: true,
|
|
250
|
+
matchedDevices: matchedDevices.length,
|
|
251
|
+
deviceTypes: uniqueTypes,
|
|
252
|
+
discoveredAt: new Date().toISOString()
|
|
253
|
+
})
|
|
254
|
+
};
|
|
255
|
+
data.unshift(summaryRow);
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
258
|
+
data.push({
|
|
259
|
+
probe_protocol: 'udp',
|
|
260
|
+
probe_port: port,
|
|
261
|
+
probe_info: 'No WS-Discovery devices relevant to target IP discovered within timeout window',
|
|
262
|
+
response_banner: JSON.stringify({
|
|
263
|
+
timeout: timeout,
|
|
264
|
+
reason: "No responses received"
|
|
265
|
+
})
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
} catch (error) {
|
|
269
|
+
dlog("WS-Discovery scan error:", error.message);
|
|
270
|
+
data.push({
|
|
271
|
+
probe_protocol: 'udp',
|
|
272
|
+
probe_port: port,
|
|
273
|
+
probe_info: 'WS-Discovery scan failed',
|
|
274
|
+
response_banner: JSON.stringify({
|
|
275
|
+
error: error.message,
|
|
276
|
+
timestamp: new Date().toISOString()
|
|
277
|
+
})
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
up,
|
|
283
|
+
program,
|
|
284
|
+
version,
|
|
285
|
+
type: 'wsdiscovery',
|
|
286
|
+
deviceCount: data.length - (up ? 1 : 0), // Exclude summary row from count
|
|
287
|
+
data
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
};
|