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,247 @@
|
|
|
1
|
+
// plugins/ftp_banner_check.mjs
|
|
2
|
+
// Real plugin to connect to an FTP server (port 21) and parse the banner for program name, version, and OS hints.
|
|
3
|
+
// Handles banners like "220 bftpd 1.6.6 at 192.168.1.1 ready." and Pure-FTPd multi-line banners.
|
|
4
|
+
// Returns { up: boolean, program: string, version: string, os: string|null, data: [{ probe_protocol, probe_port, probe_info, response_banner }] }.
|
|
5
|
+
|
|
6
|
+
import net from 'node:net';
|
|
7
|
+
|
|
8
|
+
const DEBUG = /^(1|true|yes|on)$/i.test(String(process.env.DEBUG_MODE || process.env.FTP_DEBUG || ''));
|
|
9
|
+
function dlog(...a) { if (DEBUG) console.log('[ftp-banner-check]', ...a); }
|
|
10
|
+
|
|
11
|
+
export default {
|
|
12
|
+
id: '004',
|
|
13
|
+
name: 'FTP Banner Check',
|
|
14
|
+
description: 'Connects to FTP server on port 21 and retrieves the program name, version, and OS from the banner.',
|
|
15
|
+
priority: 40,
|
|
16
|
+
requirements: { host: "up", tcp_open: [21] },
|
|
17
|
+
protocols: ['tcp'],
|
|
18
|
+
ports: [21],
|
|
19
|
+
async run(host, port = 21) {
|
|
20
|
+
const anonEnabled = /^(1|true|yes|on)$/i.test(String(process.env.FTP_CHECK_ANON || ''));
|
|
21
|
+
dlog(`Running FTP Banner Check on ${host}:${port} (anon=${anonEnabled})`);
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
const socket = new net.Socket();
|
|
24
|
+
let banner = '';
|
|
25
|
+
let up = false;
|
|
26
|
+
let anonState = 'banner'; // banner | send-user | send-pass | done
|
|
27
|
+
let anonymousLogin = false;
|
|
28
|
+
let bannerCollected = false;
|
|
29
|
+
let resolved = false;
|
|
30
|
+
const safeResolve = (value) => { if (resolved) return; resolved = true; resolve(value); };
|
|
31
|
+
|
|
32
|
+
socket.setTimeout(5000); // 5-second timeout
|
|
33
|
+
|
|
34
|
+
socket.on('connect', () => {
|
|
35
|
+
up = true; // Host is up if connection succeeds
|
|
36
|
+
dlog(`Connected to ${host}:${port}`);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
function finalizeBanner() {
|
|
40
|
+
const bannerTrimmed = banner.trim();
|
|
41
|
+
let program = 'Unknown';
|
|
42
|
+
let version = 'Unknown';
|
|
43
|
+
let os = null;
|
|
44
|
+
let probe_info = 'No recognizable FTP banner';
|
|
45
|
+
|
|
46
|
+
const singleLineMatch = bannerTrimmed.match(/220[- ]?([a-zA-Z0-9]+)\s+([0-9.]+)(?:\s+.*)?$/i) ||
|
|
47
|
+
bannerTrimmed.match(/220[- ]?\((.*?)\s+([0-9.]+)\)/i);
|
|
48
|
+
const pureFtpdMatch = bannerTrimmed.match(/220[-]+ Welcome to ([a-zA-Z0-9-]+)(?:\s+\[.*?\])?[- ]+/i);
|
|
49
|
+
|
|
50
|
+
if (singleLineMatch) {
|
|
51
|
+
program = singleLineMatch[1] || 'Unknown';
|
|
52
|
+
version = singleLineMatch[2] || 'Unknown';
|
|
53
|
+
probe_info = `Detected FTP server: ${program} ${version}`;
|
|
54
|
+
dlog(`Parsed banner: program=${program}, version=${version}`);
|
|
55
|
+
} else if (pureFtpdMatch) {
|
|
56
|
+
program = pureFtpdMatch[1] || 'Unknown';
|
|
57
|
+
probe_info = `Detected FTP server: ${program} (version not specified in banner)`;
|
|
58
|
+
dlog(`Parsed banner: program=${program}, version=Unknown`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (bannerTrimmed.toLowerCase().includes('unix') || program.toLowerCase().includes('pure-ftpd') || program.toLowerCase().includes('vsftpd') || program.toLowerCase().includes('bftpd')) {
|
|
62
|
+
os = 'Linux';
|
|
63
|
+
dlog(`OS detected: ${os} from banner or program`);
|
|
64
|
+
} else if (bannerTrimmed.toLowerCase().includes('windows') || program.toLowerCase().includes('filezilla')) {
|
|
65
|
+
os = 'Windows';
|
|
66
|
+
dlog(`OS detected: ${os} from banner or program`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { bannerTrimmed, program, version, os, probe_info };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function buildResult(parsed) {
|
|
73
|
+
const evidenceRows = [{
|
|
74
|
+
probe_protocol: 'tcp',
|
|
75
|
+
probe_port: port,
|
|
76
|
+
probe_info: parsed.probe_info,
|
|
77
|
+
response_banner: parsed.bannerTrimmed || null
|
|
78
|
+
}];
|
|
79
|
+
|
|
80
|
+
if (anonEnabled && anonymousLogin) {
|
|
81
|
+
evidenceRows.push({
|
|
82
|
+
probe_protocol: 'tcp',
|
|
83
|
+
probe_port: port,
|
|
84
|
+
probe_info: 'SECURITY FINDING: Anonymous FTP login permitted',
|
|
85
|
+
response_banner: parsed.bannerTrimmed || null
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const result = {
|
|
90
|
+
up,
|
|
91
|
+
program: parsed.program,
|
|
92
|
+
version: parsed.version,
|
|
93
|
+
os: parsed.os,
|
|
94
|
+
data: evidenceRows
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
if (anonEnabled) {
|
|
98
|
+
result.anonymousLogin = anonymousLogin;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
dlog(`FTP Banner Check result: up=${up}, program=${parsed.program}, version=${parsed.version}, os=${parsed.os}, anonymousLogin=${anonymousLogin}, banner=${parsed.bannerTrimmed || 'none'}`);
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const MAX_BANNER = 65536;
|
|
106
|
+
socket.on('data', (data) => {
|
|
107
|
+
const chunk = data.toString();
|
|
108
|
+
|
|
109
|
+
if (anonState === 'banner') {
|
|
110
|
+
if (banner.length > MAX_BANNER) { socket.destroy(); return; }
|
|
111
|
+
banner += chunk;
|
|
112
|
+
if (bannerCollected) return; // already scheduled
|
|
113
|
+
bannerCollected = true;
|
|
114
|
+
// Collect banner for up to 1 second to handle multi-line banners like Pure-FTPd
|
|
115
|
+
setTimeout(() => {
|
|
116
|
+
if (anonEnabled && banner.trim().startsWith('220')) {
|
|
117
|
+
anonState = 'send-user';
|
|
118
|
+
dlog('Starting anonymous login check');
|
|
119
|
+
socket.write('USER anonymous\r\n');
|
|
120
|
+
// Set a separate timeout for the anonymous check
|
|
121
|
+
setTimeout(() => {
|
|
122
|
+
if (anonState !== 'done') {
|
|
123
|
+
dlog('Anonymous login check timed out');
|
|
124
|
+
anonState = 'done';
|
|
125
|
+
socket.end();
|
|
126
|
+
}
|
|
127
|
+
}, 3000);
|
|
128
|
+
} else {
|
|
129
|
+
socket.end();
|
|
130
|
+
}
|
|
131
|
+
}, 1000);
|
|
132
|
+
} else if (anonState === 'send-user') {
|
|
133
|
+
const response = chunk.trim();
|
|
134
|
+
dlog(`Anon USER response: ${response}`);
|
|
135
|
+
if (response.startsWith('331')) {
|
|
136
|
+
anonState = 'send-pass';
|
|
137
|
+
socket.write('PASS anonymous@audit.local\r\n');
|
|
138
|
+
} else if (response.startsWith('230')) {
|
|
139
|
+
// Logged in without password
|
|
140
|
+
anonymousLogin = true;
|
|
141
|
+
anonState = 'done';
|
|
142
|
+
socket.end();
|
|
143
|
+
} else {
|
|
144
|
+
// Unexpected response, treat as denied
|
|
145
|
+
anonState = 'done';
|
|
146
|
+
socket.end();
|
|
147
|
+
}
|
|
148
|
+
} else if (anonState === 'send-pass') {
|
|
149
|
+
const response = chunk.trim();
|
|
150
|
+
dlog(`Anon PASS response: ${response}`);
|
|
151
|
+
if (response.startsWith('230')) {
|
|
152
|
+
anonymousLogin = true;
|
|
153
|
+
}
|
|
154
|
+
// 530/421 or anything else = denied
|
|
155
|
+
anonState = 'done';
|
|
156
|
+
socket.end();
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
socket.on('end', () => {
|
|
161
|
+
const parsed = finalizeBanner();
|
|
162
|
+
safeResolve(buildResult(parsed));
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
socket.on('timeout', () => {
|
|
166
|
+
socket.destroy();
|
|
167
|
+
dlog(`FTP connection to ${host}:${port} timed out`);
|
|
168
|
+
safeResolve({
|
|
169
|
+
up: false,
|
|
170
|
+
program: 'Unknown',
|
|
171
|
+
version: 'Unknown',
|
|
172
|
+
os: null,
|
|
173
|
+
data: [{
|
|
174
|
+
probe_protocol: 'tcp',
|
|
175
|
+
probe_port: port,
|
|
176
|
+
probe_info: 'Connection timed out',
|
|
177
|
+
response_banner: null
|
|
178
|
+
}]
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
socket.on('error', (err) => {
|
|
183
|
+
socket.destroy();
|
|
184
|
+
let probe_info = `Connection error: ${err.message}`;
|
|
185
|
+
if (err.code === 'ECONNREFUSED') {
|
|
186
|
+
up = true; // Host is up but port is closed
|
|
187
|
+
probe_info = 'Connection refused - host up, FTP port closed';
|
|
188
|
+
dlog(`FTP connection to ${host}:${port} refused - host up`);
|
|
189
|
+
} else {
|
|
190
|
+
dlog(`FTP connection error to ${host}:${port}: ${err.message}`);
|
|
191
|
+
}
|
|
192
|
+
safeResolve({
|
|
193
|
+
up,
|
|
194
|
+
program: 'Unknown',
|
|
195
|
+
version: 'Unknown',
|
|
196
|
+
os: null,
|
|
197
|
+
data: [{
|
|
198
|
+
probe_protocol: 'tcp',
|
|
199
|
+
probe_port: port,
|
|
200
|
+
probe_info,
|
|
201
|
+
response_banner: null
|
|
202
|
+
}]
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
dlog(`Attempting TCP connection to ${host}:${port}`);
|
|
207
|
+
socket.connect(port, host);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// Plug-and-play adapter for the concluder
|
|
213
|
+
import { statusFrom } from '../utils/conclusion_utils.mjs';
|
|
214
|
+
|
|
215
|
+
export async function conclude({ host, result }) {
|
|
216
|
+
const rows = Array.isArray(result?.data) ? result.data : [];
|
|
217
|
+
// Prefer first meaningful row
|
|
218
|
+
const r = rows.find(x => x && (x.probe_port != null)) || rows[0] || {};
|
|
219
|
+
const info = r?.probe_info || null;
|
|
220
|
+
const banner = r?.response_banner || null;
|
|
221
|
+
|
|
222
|
+
// Derive status: treat ECONNREFUSED/refused as 'closed'; banner '220' as 'open'
|
|
223
|
+
let status = statusFrom({ info, banner });
|
|
224
|
+
if (status === 'unknown' && result?.up === true && banner) status = 'open';
|
|
225
|
+
|
|
226
|
+
const record = {
|
|
227
|
+
port: Number(r?.probe_port ?? 21),
|
|
228
|
+
protocol: String(r?.probe_protocol || 'tcp'),
|
|
229
|
+
service: 'ftp',
|
|
230
|
+
program: result?.program || 'Unknown',
|
|
231
|
+
version: result?.version || 'Unknown',
|
|
232
|
+
status,
|
|
233
|
+
info,
|
|
234
|
+
banner,
|
|
235
|
+
source: 'ftp',
|
|
236
|
+
evidence: rows,
|
|
237
|
+
authoritative: true
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
if (result?.anonymousLogin != null) {
|
|
241
|
+
record.anonymousLogin = result.anonymousLogin;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return [record];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export const authoritativePorts = new Set(['tcp:21']);
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
// plugins/host_up_check.mjs
|
|
2
|
+
// Real plugin to check if a host is up or down using ICMP (ping), TCP (common ports), and UDP (high closed port) probes.
|
|
3
|
+
// Updated to prioritize ICMP TTL-based OS detection, with TCP probes refining ambiguous TTLs (e.g., TTL 64) using banners and port heuristics.
|
|
4
|
+
// Extracts router name and version from port 443 banner (e.g., Netgear R8000) and includes in result.
|
|
5
|
+
// Returns { up: boolean, os: string|null, router_info: { name: string|null, version: string|null }|null, data: [{ probe_protocol, probe_port, probe_info, response_banner }] }.
|
|
6
|
+
|
|
7
|
+
import { promisify } from 'node:util';
|
|
8
|
+
import { execFile } from 'node:child_process';
|
|
9
|
+
import net from 'node:net';
|
|
10
|
+
import dgram from 'node:dgram';
|
|
11
|
+
|
|
12
|
+
const execFileP = promisify(execFile);
|
|
13
|
+
|
|
14
|
+
const DEBUG = /^(1|true|yes|on)$/i.test(String(process.env.DEBUG_MODE || process.env.HOST_UP_DEBUG || ''));
|
|
15
|
+
function dlog(...a) { if (DEBUG) console.log('[host-up-check]', ...a); }
|
|
16
|
+
|
|
17
|
+
/** Validate host string to prevent command injection. */
|
|
18
|
+
function isValidHost(h) {
|
|
19
|
+
if (!h || typeof h !== "string") return false;
|
|
20
|
+
return /^[a-zA-Z0-9.:_\-\[\]%]+$/.test(h);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// TTL to OS mapping based on provided data (filtered for ICMP, removed Netgear FVG318)
|
|
24
|
+
const TTL_OS_MAPPING = [
|
|
25
|
+
{ Device_OS: "AIX", TTL: 255, Protocol: "ICMP" },
|
|
26
|
+
{ Device_OS: "BSDI", TTL: 255, Protocol: "ICMP" },
|
|
27
|
+
{ Device_OS: "Compa", TTL: 64, Protocol: "ICMP" },
|
|
28
|
+
{ Device_OS: "Cisco", TTL: 254, Protocol: "ICMP" },
|
|
29
|
+
{ Device_OS: "Foundry", TTL: 64, Protocol: "ICMP" },
|
|
30
|
+
{ Device_OS: "FreeBSD", TTL: 255, Protocol: "ICMP" },
|
|
31
|
+
{ Device_OS: "FreeBSD", TTL: 64, Protocol: "ICMP" },
|
|
32
|
+
{ Device_OS: "HP-UX", TTL: 255, Protocol: "ICMP" },
|
|
33
|
+
{ Device_OS: "Irix", TTL: 255, Protocol: "ICMP" },
|
|
34
|
+
{ Device_OS: "juniper", TTL: 64, Protocol: "ICMP" },
|
|
35
|
+
{ Device_OS: "MPE/IX (HP)", TTL: 200, Protocol: "ICMP" },
|
|
36
|
+
{ Device_OS: "Linux", TTL: 64, Protocol: "ICMP" },
|
|
37
|
+
{ Device_OS: "Linux", TTL: 255, Protocol: "ICMP" },
|
|
38
|
+
{ Device_OS: "MacOS/MacTCP", TTL: 64, Protocol: "ICMP/TCP/UDP" },
|
|
39
|
+
{ Device_OS: "NetBSD", TTL: 255, Protocol: "ICMP" },
|
|
40
|
+
{ Device_OS: "OpenBSD", TTL: 255, Protocol: "ICMP" },
|
|
41
|
+
{ Device_OS: "OpenVMS", TTL: 255, Protocol: "ICMP" },
|
|
42
|
+
{ Device_OS: "Solaris", TTL: 255, Protocol: "ICMP" },
|
|
43
|
+
{ Device_OS: "Stratus", TTL: 255, Protocol: "ICMP" },
|
|
44
|
+
{ Device_OS: "SunOS", TTL: 255, Protocol: "ICMP" },
|
|
45
|
+
{ Device_OS: "Ultrix", TTL: 255, Protocol: "ICMP" },
|
|
46
|
+
{ Device_OS: "Windows", TTL: 32, Protocol: "ICMP" },
|
|
47
|
+
{ Device_OS: "Windows", TTL: 128, Protocol: "ICMP" },
|
|
48
|
+
{ Device_OS: "Android", TTL: 64, Protocol: "TCP and ICMP" }
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
// Prioritize Linux for router-like devices with TTL 64
|
|
52
|
+
const OS_SPECIFICITY = {
|
|
53
|
+
"Linux": 10, // High for servers/routers with FTP/HTTP/HTTPS
|
|
54
|
+
"MacOS/MacTCP": 9, // High for desktops
|
|
55
|
+
"Android": 8,
|
|
56
|
+
"FreeBSD": 7,
|
|
57
|
+
"juniper": 6,
|
|
58
|
+
"Cisco": 6,
|
|
59
|
+
"Foundry": 5,
|
|
60
|
+
"Windows": 4,
|
|
61
|
+
"AIX": 3,
|
|
62
|
+
"BSDI": 3,
|
|
63
|
+
"Compa": 3,
|
|
64
|
+
"HP-UX": 3,
|
|
65
|
+
"Irix": 3,
|
|
66
|
+
"MPE/IX (HP)": 3,
|
|
67
|
+
"NetBSD": 3,
|
|
68
|
+
"OpenBSD": 3,
|
|
69
|
+
"OpenVMS": 3,
|
|
70
|
+
"Solaris": 3,
|
|
71
|
+
"Stratus": 3,
|
|
72
|
+
"SunOS": 3,
|
|
73
|
+
"Ultrix": 3
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export default {
|
|
77
|
+
id: '005',
|
|
78
|
+
name: 'Host Up Check',
|
|
79
|
+
description: 'Checks if the host is up or down using ICMP, TCP (common ports), and UDP (high closed port) probes with enhanced OS detection.',
|
|
80
|
+
priority: 20,
|
|
81
|
+
requirements: { host: "down" }, // run when ping hasn't marked it UP (ping blocked/filtered)
|
|
82
|
+
protocols: ['tcp', 'udp', 'icmp'],
|
|
83
|
+
ports: [21, 22, 80, 443, 3389, 54321],
|
|
84
|
+
runStrategy: 'single',
|
|
85
|
+
async run(host) {
|
|
86
|
+
if (!isValidHost(host)) {
|
|
87
|
+
return { up: false, os: null, router_info: null, data: [{ probe_protocol: 'icmp', probe_port: null, probe_info: 'Invalid host', response_banner: null }] };
|
|
88
|
+
}
|
|
89
|
+
dlog(`Running Host Up Check on ${host}`);
|
|
90
|
+
const data = [];
|
|
91
|
+
let up = false;
|
|
92
|
+
let os = null;
|
|
93
|
+
let router_info = null; // { name: string|null, version: string|null }
|
|
94
|
+
let icmpCandidates = []; // Store ICMP candidates for TCP refinement
|
|
95
|
+
|
|
96
|
+
// ICMP Probe (using system ping)
|
|
97
|
+
try {
|
|
98
|
+
const isWindows = process.platform === 'win32';
|
|
99
|
+
const pingArgs = isWindows ? ['-n', '1', '-w', '1000', host] : ['-c', '1', '-W', '1', host];
|
|
100
|
+
dlog(`Executing ping command: ping ${pingArgs.join(' ')}`);
|
|
101
|
+
const { stdout } = await execFileP('ping', pingArgs, { windowsHide: true, timeout: 6000 });
|
|
102
|
+
const received = isWindows ? stdout.includes('Reply from') : stdout.includes('1 received');
|
|
103
|
+
let probe_info = received ? 'Ping successful' : 'Ping failed';
|
|
104
|
+
if (received) {
|
|
105
|
+
up = true;
|
|
106
|
+
// Enhanced OS detection from TTL
|
|
107
|
+
const ttlMatch = stdout.match(/ttl=(\d+)/i);
|
|
108
|
+
if (ttlMatch) {
|
|
109
|
+
const ttl = parseInt(ttlMatch[1], 10);
|
|
110
|
+
icmpCandidates = TTL_OS_MAPPING.filter(entry => entry.TTL === ttl && entry.Protocol.includes('ICMP'));
|
|
111
|
+
dlog(`ICMP TTL ${ttl} candidates: ${icmpCandidates.map(c => c.Device_OS).join(', ')}`);
|
|
112
|
+
if (icmpCandidates.length > 0) {
|
|
113
|
+
// Choose the most specific OS based on OS_SPECIFICITY
|
|
114
|
+
os = icmpCandidates.reduce((best, curr) =>
|
|
115
|
+
(OS_SPECIFICITY[curr.Device_OS] || 0) > (OS_SPECIFICITY[best.Device_OS] || 0) ? curr : best
|
|
116
|
+
).Device_OS;
|
|
117
|
+
probe_info = `Ping successful (TTL: ${ttl}, OS: ${os})`;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
data.push({
|
|
122
|
+
probe_protocol: 'icmp',
|
|
123
|
+
probe_port: null,
|
|
124
|
+
probe_info,
|
|
125
|
+
response_banner: null
|
|
126
|
+
});
|
|
127
|
+
} catch (err) {
|
|
128
|
+
if (DEBUG) console.error('[host-up-check]', `ICMP probe error: ${err.message}`);
|
|
129
|
+
data.push({
|
|
130
|
+
probe_protocol: 'icmp',
|
|
131
|
+
probe_port: null,
|
|
132
|
+
probe_info: `Ping error: ${err.message}`,
|
|
133
|
+
response_banner: null
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// TCP Probes (common ports)
|
|
138
|
+
const tcpPorts = [21, 22, 80, 443, 3389];
|
|
139
|
+
for (const port of tcpPorts) {
|
|
140
|
+
await new Promise((resolve) => {
|
|
141
|
+
dlog(`Attempting TCP probe on ${host}:${port}`);
|
|
142
|
+
const socket = new net.Socket();
|
|
143
|
+
socket.setTimeout(2000);
|
|
144
|
+
let banner = '';
|
|
145
|
+
|
|
146
|
+
socket.on('connect', () => {
|
|
147
|
+
up = true;
|
|
148
|
+
const MAX_BANNER = 4096;
|
|
149
|
+
socket.on('data', (d) => { if (banner.length < MAX_BANNER) banner += d.toString(); });
|
|
150
|
+
setTimeout(() => {
|
|
151
|
+
// OS detection from banner or port, only if ICMP is ambiguous
|
|
152
|
+
let newOs = null;
|
|
153
|
+
let newRouterInfo = null;
|
|
154
|
+
if (banner.toLowerCase().includes('unix') || banner.toLowerCase().includes('linux') || banner.toLowerCase().includes('bftpd')) {
|
|
155
|
+
newOs = 'Linux';
|
|
156
|
+
dlog(`TCP probe port ${port}: Detected ${newOs} from banner: ${banner.trim()}`);
|
|
157
|
+
} else if (banner.toLowerCase().includes('windows') || banner.includes('Microsoft')) {
|
|
158
|
+
newOs = 'Windows';
|
|
159
|
+
dlog(`TCP probe port ${port}: Detected ${newOs} from banner: ${banner.trim()}`);
|
|
160
|
+
} else if (banner.toLowerCase().includes('netgear')) {
|
|
161
|
+
newOs = 'Linux'; // Netgear routers typically run Linux
|
|
162
|
+
// Extract router name and version (e.g., Netgear R8000)
|
|
163
|
+
const routerMatch = banner.match(/(netgear)\s+([^\s]+)/i);
|
|
164
|
+
if (routerMatch) {
|
|
165
|
+
newRouterInfo = { name: routerMatch[1], version: routerMatch[2] };
|
|
166
|
+
dlog(`TCP probe port ${port}: Detected router ${newRouterInfo.name} ${newRouterInfo.version} from banner: ${banner.trim()}`);
|
|
167
|
+
} else {
|
|
168
|
+
newRouterInfo = { name: 'Netgear', version: null };
|
|
169
|
+
dlog(`TCP probe port ${port}: Detected generic Netgear router from banner: ${banner.trim()}`);
|
|
170
|
+
}
|
|
171
|
+
} else if (port === 3389) {
|
|
172
|
+
newOs = 'Windows'; // RDP port suggests Windows
|
|
173
|
+
} else if (port === 22) {
|
|
174
|
+
newOs = 'MacOS/MacTCP'; // SSH port suggests MacOS on desktops
|
|
175
|
+
} else if (port === 21 || port === 80 || port === 443) {
|
|
176
|
+
newOs = 'Linux'; // FTP/HTTP/HTTPS ports suggest Linux on routers
|
|
177
|
+
dlog(`TCP probe port ${port}: Detected ${newOs} from open port (router heuristic)`);
|
|
178
|
+
}
|
|
179
|
+
// Refine os if ICMP set an ambiguous TTL (e.g., 64) or newOs is more specific
|
|
180
|
+
if (newOs && (!os || (icmpCandidates.length > 1 && (OS_SPECIFICITY[newOs] || 0) >= (OS_SPECIFICITY[os] || 0)))) {
|
|
181
|
+
dlog(`TCP probe port ${port}: Setting os=${newOs} (ICMP ambiguous or less specific, TTL ${icmpCandidates[0]?.TTL})`);
|
|
182
|
+
os = newOs;
|
|
183
|
+
if (newRouterInfo) {
|
|
184
|
+
router_info = newRouterInfo;
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
dlog(`TCP probe port ${port}: Keeping os=${os} (newOs=${newOs})`);
|
|
188
|
+
}
|
|
189
|
+
data.push({
|
|
190
|
+
probe_protocol: 'tcp',
|
|
191
|
+
probe_port: port,
|
|
192
|
+
probe_info: 'Connection successful',
|
|
193
|
+
response_banner: banner.trim() || null
|
|
194
|
+
});
|
|
195
|
+
socket.destroy();
|
|
196
|
+
resolve();
|
|
197
|
+
}, 1000);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
socket.on('error', (err) => {
|
|
201
|
+
if (err.code === 'ECONNREFUSED') {
|
|
202
|
+
up = true;
|
|
203
|
+
let newOs = null;
|
|
204
|
+
if (port === 3389) {
|
|
205
|
+
newOs = 'Windows'; // Refused RDP port suggests Windows
|
|
206
|
+
} else if (port === 22) {
|
|
207
|
+
newOs = 'MacOS/MacTCP'; // Refused SSH port suggests MacOS
|
|
208
|
+
}
|
|
209
|
+
// Refine os if ICMP set an ambiguous TTL (e.g., 64) or newOs is more specific
|
|
210
|
+
if (newOs && (!os || (icmpCandidates.length > 1 && (OS_SPECIFICITY[newOs] || 0) >= (OS_SPECIFICITY[os] || 0)))) {
|
|
211
|
+
dlog(`TCP probe port ${port}: Setting os=${newOs} (ICMP ambiguous or less specific, TTL ${icmpCandidates[0]?.TTL})`);
|
|
212
|
+
os = newOs;
|
|
213
|
+
} else {
|
|
214
|
+
dlog(`TCP probe port ${port}: Keeping os=${os} (newOs=${newOs})`);
|
|
215
|
+
}
|
|
216
|
+
data.push({
|
|
217
|
+
probe_protocol: 'tcp',
|
|
218
|
+
probe_port: port,
|
|
219
|
+
probe_info: 'Connection refused - host up',
|
|
220
|
+
response_banner: null
|
|
221
|
+
});
|
|
222
|
+
} else {
|
|
223
|
+
data.push({
|
|
224
|
+
probe_protocol: 'tcp',
|
|
225
|
+
probe_port: port,
|
|
226
|
+
probe_info: `Error: ${err.code} - ${err.message}`,
|
|
227
|
+
response_banner: null
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
resolve();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
socket.on('timeout', () => {
|
|
234
|
+
data.push({
|
|
235
|
+
probe_protocol: 'tcp',
|
|
236
|
+
probe_port: port,
|
|
237
|
+
probe_info: 'Timeout',
|
|
238
|
+
response_banner: null
|
|
239
|
+
});
|
|
240
|
+
socket.destroy();
|
|
241
|
+
resolve();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
socket.connect(port, host);
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// UDP Probe (send to likely closed high port)
|
|
249
|
+
const udpPort = 54321;
|
|
250
|
+
await new Promise((resolve) => {
|
|
251
|
+
dlog(`Attempting UDP probe on ${host}:${udpPort}`);
|
|
252
|
+
const socket = dgram.createSocket('udp4');
|
|
253
|
+
const timeoutId = setTimeout(() => {
|
|
254
|
+
data.push({
|
|
255
|
+
probe_protocol: 'udp',
|
|
256
|
+
probe_port: udpPort,
|
|
257
|
+
probe_info: 'Timeout - host possibly down',
|
|
258
|
+
response_banner: null
|
|
259
|
+
});
|
|
260
|
+
socket.close();
|
|
261
|
+
resolve();
|
|
262
|
+
}, 3000);
|
|
263
|
+
|
|
264
|
+
socket.on('error', (err) => {
|
|
265
|
+
clearTimeout(timeoutId);
|
|
266
|
+
if (err.code === 'ECONNREFUSED') {
|
|
267
|
+
up = true;
|
|
268
|
+
data.push({
|
|
269
|
+
probe_protocol: 'udp',
|
|
270
|
+
probe_port: udpPort,
|
|
271
|
+
probe_info: 'ICMP Port Unreachable - host up',
|
|
272
|
+
response_banner: null
|
|
273
|
+
});
|
|
274
|
+
} else {
|
|
275
|
+
data.push({
|
|
276
|
+
probe_protocol: 'udp',
|
|
277
|
+
probe_port: udpPort,
|
|
278
|
+
probe_info: `Error: ${err.code} - ${err.message}`,
|
|
279
|
+
response_banner: null
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
socket.close();
|
|
283
|
+
resolve();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
socket.connect(udpPort, host, (err) => {
|
|
287
|
+
if (err) {
|
|
288
|
+
clearTimeout(timeoutId);
|
|
289
|
+
data.push({
|
|
290
|
+
probe_protocol: 'udp',
|
|
291
|
+
probe_port: udpPort,
|
|
292
|
+
probe_info: `Connect error: ${err.message}`,
|
|
293
|
+
response_banner: null
|
|
294
|
+
});
|
|
295
|
+
socket.close();
|
|
296
|
+
resolve();
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
socket.send(Buffer.alloc(0), (err) => {
|
|
301
|
+
clearTimeout(timeoutId);
|
|
302
|
+
if (err) {
|
|
303
|
+
if (err.code === 'ECONNREFUSED') {
|
|
304
|
+
up = true;
|
|
305
|
+
data.push({
|
|
306
|
+
probe_protocol: 'udp',
|
|
307
|
+
probe_port: udpPort,
|
|
308
|
+
probe_info: 'ICMP Port Unreachable - host up',
|
|
309
|
+
response_banner: null
|
|
310
|
+
});
|
|
311
|
+
} else {
|
|
312
|
+
data.push({
|
|
313
|
+
probe_protocol: 'udp',
|
|
314
|
+
probe_port: udpPort,
|
|
315
|
+
probe_info: `Send error: ${err.message}`,
|
|
316
|
+
response_banner: null
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
} else {
|
|
320
|
+
up = true;
|
|
321
|
+
data.push({
|
|
322
|
+
probe_protocol: 'udp',
|
|
323
|
+
probe_port: udpPort,
|
|
324
|
+
probe_info: 'Send successful, no error - host up (port may be open)',
|
|
325
|
+
response_banner: null
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
socket.close();
|
|
329
|
+
resolve();
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
dlog(`Host Up Check result: up=${up}, os=${os}, router_info=${JSON.stringify(router_info)}, data=${JSON.stringify(data)}`);
|
|
335
|
+
return { up, os, router_info, data };
|
|
336
|
+
}
|
|
337
|
+
};
|