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,737 @@
|
|
|
1
|
+
// plugins/netbios_scanner.mjs
|
|
2
|
+
// NetBIOS/SMB discovery plugin (UDP 137 NBNS, TCP 139/445 probes, and optional mDNS _smb._tcp.local)
|
|
3
|
+
// Optional SMB2 null session enumeration (gated by SMB_NULL_SESSION env var).
|
|
4
|
+
// Exports helpers used by tests: parseNbstatRData, buildMdnsQueryPTR, buildSmb2Negotiate,
|
|
5
|
+
// parseSmb2NegotiateResponse, buildSmb2SessionSetup, buildSmb2TreeConnect,
|
|
6
|
+
// parseSmb2Header, buildNetShareEnumAll, buildSamrConnect, parseNetShareEnumAllResponse,
|
|
7
|
+
// parseSamrEnumUsersResponse.
|
|
8
|
+
// Focus is correctness and small footprint suitable for unit tests.
|
|
9
|
+
|
|
10
|
+
import dgram from 'node:dgram';
|
|
11
|
+
import net from 'node:net';
|
|
12
|
+
|
|
13
|
+
/* ----------------------------- Helpers ----------------------------- */
|
|
14
|
+
|
|
15
|
+
function toHex(b){ return [...b].map(x=>x.toString(16).padStart(2,'0')).join(''); }
|
|
16
|
+
|
|
17
|
+
// Parse NBSTAT RDATA (RFC 1002) which is: <NUM_NAMES:1> [<NAME:15><SUFFIX:1><FLAGS:2>]... <UNIT_ID:6>
|
|
18
|
+
export function parseNbstatRData(buf){
|
|
19
|
+
const out = { names: [], mac: null };
|
|
20
|
+
if (!buf || buf.length < 1+6) return out;
|
|
21
|
+
let off = 0;
|
|
22
|
+
const num = buf.readUInt8(off++);
|
|
23
|
+
for (let i=0;i<num;i++){
|
|
24
|
+
if (off + 15 + 1 + 2 > buf.length) break;
|
|
25
|
+
const rawName = buf.subarray(off, off+15); off+=15;
|
|
26
|
+
const suffix = buf.readUInt8(off++);
|
|
27
|
+
const flags = buf.readUInt16BE(off); off+=2;
|
|
28
|
+
// Trim trailing spaces from 15-byte NetBIOS name
|
|
29
|
+
const name = rawName.toString('ascii').replace(/\s+$/g,'');
|
|
30
|
+
out.names.push({ name, suffix, flags, group: !!(flags & 0x8000) });
|
|
31
|
+
}
|
|
32
|
+
if (off + 6 <= buf.length){
|
|
33
|
+
const mac = buf.subarray(off, off+6);
|
|
34
|
+
out.mac = toHex(mac);
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Build an mDNS QU question for PTR _smb._tcp.local (ID=0, Flags=0, QDCOUNT=1)
|
|
40
|
+
export function buildMdnsQueryPTR(){
|
|
41
|
+
const labels = ['_smb','_tcp','local'];
|
|
42
|
+
const nameParts = [];
|
|
43
|
+
for (const l of labels){
|
|
44
|
+
const b = Buffer.from(l,'ascii');
|
|
45
|
+
nameParts.push(Buffer.from([b.length]));
|
|
46
|
+
nameParts.push(b);
|
|
47
|
+
}
|
|
48
|
+
nameParts.push(Buffer.from([0])); // root
|
|
49
|
+
const qname = Buffer.concat(nameParts);
|
|
50
|
+
const header = Buffer.alloc(12);
|
|
51
|
+
// ID=0, FLAGS=0 for mDNS multicast query
|
|
52
|
+
header.writeUInt16BE(0, 0);
|
|
53
|
+
header.writeUInt16BE(0, 2);
|
|
54
|
+
header.writeUInt16BE(1, 4); // QDCOUNT
|
|
55
|
+
header.writeUInt16BE(0, 6); // ANCOUNT
|
|
56
|
+
header.writeUInt16BE(0, 8); // NSCOUNT
|
|
57
|
+
header.writeUInt16BE(0, 10);// ARCOUNT
|
|
58
|
+
const qtype = Buffer.alloc(2); qtype.writeUInt16BE(12,0); // PTR
|
|
59
|
+
// CLASS IN (1) with QU (unicast-response) bit set
|
|
60
|
+
const qclass = Buffer.alloc(2); qclass.writeUInt16BE(0x8000 | 1, 0);
|
|
61
|
+
return Buffer.concat([header, qname, qtype, qclass]);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Build a minimal SMB2 NEGOTIATE request (MS-SMB2 §2.2.3)
|
|
65
|
+
export function buildSmb2Negotiate(){
|
|
66
|
+
const hdr = Buffer.alloc(64, 0);
|
|
67
|
+
hdr.writeUInt32BE(0xfe534d42, 0); // SMB2 signature
|
|
68
|
+
hdr.writeUInt16LE(64, 4); // StructureSize
|
|
69
|
+
hdr.writeUInt16LE(1, 6); // CreditCharge
|
|
70
|
+
// Command = NEGOTIATE (0x0000) already zero at offset 12
|
|
71
|
+
hdr.writeUInt16LE(1, 14); // Credits requested
|
|
72
|
+
// Flags (offset 16) and NextCommand (offset 20) are zero from alloc
|
|
73
|
+
const body = Buffer.alloc(38, 0); // 36 fixed fields + 2 bytes for dialect
|
|
74
|
+
body.writeUInt16LE(36, 0); // StructureSize
|
|
75
|
+
body.writeUInt16LE(1, 2); // DialectCount
|
|
76
|
+
body.writeUInt16LE(0x0202, 36); // Dialect 2.02 (after 36-byte fixed header)
|
|
77
|
+
return Buffer.concat([hdr, body]);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function parseSmb2NegotiateResponse(buf){
|
|
81
|
+
// trivial check: first 4 bytes == 0xfe 'S' 'M' 'B'
|
|
82
|
+
if (!buf || buf.length < 4) return { ok:false };
|
|
83
|
+
const sig = buf.readUInt32BE(0);
|
|
84
|
+
const ok = sig === 0xfe534d42;
|
|
85
|
+
return { ok, dialects: ok ? 1 : 0 };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Prepend 4-byte NetBIOS Session Service header (RFC 1002 §4.3.1)
|
|
89
|
+
// SMB2 over TCP/445 requires this framing for all messages.
|
|
90
|
+
function wrapNbss(smb2Packet) {
|
|
91
|
+
const hdr = Buffer.alloc(4);
|
|
92
|
+
hdr.writeUInt32BE(smb2Packet.length, 0);
|
|
93
|
+
hdr[0] = 0x00; // Session Message type
|
|
94
|
+
return Buffer.concat([hdr, smb2Packet]);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Strip 4-byte NBSS header if present, returning the SMB2 payload.
|
|
98
|
+
function stripNbss(buf) {
|
|
99
|
+
if (buf.length > 4 && buf[0] === 0x00) return buf.subarray(4);
|
|
100
|
+
return buf;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* -------------------- SMB2 Null Session helpers -------------------- */
|
|
104
|
+
|
|
105
|
+
// SMB2 status codes
|
|
106
|
+
const STATUS_SUCCESS = 0x00000000;
|
|
107
|
+
const STATUS_MORE_PROCESSING = 0xC0000016;
|
|
108
|
+
const STATUS_ACCESS_DENIED = 0xC0000022;
|
|
109
|
+
|
|
110
|
+
// Parse an SMB2 header (64 bytes) — returns status, sessionId, treeId, command
|
|
111
|
+
export function parseSmb2Header(buf){
|
|
112
|
+
if (!buf || buf.length < 64) return null;
|
|
113
|
+
const sig = buf.readUInt32BE(0);
|
|
114
|
+
if (sig !== 0xfe534d42) return null;
|
|
115
|
+
const command = buf.readUInt16LE(12);
|
|
116
|
+
const status = buf.readUInt32LE(8);
|
|
117
|
+
// SessionId is at offset 40 (8 bytes), read as pair of UInt32LE
|
|
118
|
+
const sessionIdLo = buf.readUInt32LE(40);
|
|
119
|
+
const sessionIdHi = buf.readUInt32LE(44);
|
|
120
|
+
const treeId = buf.readUInt32LE(36);
|
|
121
|
+
return { command, status, sessionIdLo, sessionIdHi, treeId };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Build NTLMSSP Negotiate token (Type 1) for anonymous auth
|
|
125
|
+
function buildNtlmsspNegotiate(){
|
|
126
|
+
// NTLMSSP_NEGOTIATE (type 1)
|
|
127
|
+
const sig = Buffer.from('NTLMSSP\0', 'ascii'); // 8 bytes
|
|
128
|
+
const type = Buffer.alloc(4); type.writeUInt32LE(1, 0);
|
|
129
|
+
// Flags: NTLMSSP_NEGOTIATE_UNICODE | NTLMSSP_REQUEST_TARGET | NTLMSSP_NEGOTIATE_NTLM
|
|
130
|
+
const flags = Buffer.alloc(4); flags.writeUInt32LE(0x00000207, 0);
|
|
131
|
+
// Domain name fields (empty)
|
|
132
|
+
const domLen = Buffer.alloc(4, 0); // Len(2) + MaxLen(2)
|
|
133
|
+
const domOff = Buffer.alloc(4); domOff.writeUInt32LE(0, 0);
|
|
134
|
+
// Workstation fields (empty)
|
|
135
|
+
const wkLen = Buffer.alloc(4, 0);
|
|
136
|
+
const wkOff = Buffer.alloc(4); wkOff.writeUInt32LE(0, 0);
|
|
137
|
+
return Buffer.concat([sig, type, flags, domLen, domOff, wkLen, wkOff]);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Build NTLMSSP Authenticate token (Type 3) with null credentials
|
|
141
|
+
function buildNtlmsspAuth(){
|
|
142
|
+
const sig = Buffer.from('NTLMSSP\0', 'ascii');
|
|
143
|
+
const type = Buffer.alloc(4); type.writeUInt32LE(3, 0);
|
|
144
|
+
const payloadOffset = 8 + 4 + (6 * 8) + 4; // 88 bytes fixed header
|
|
145
|
+
// All security buffers empty (Len=0, MaxLen=0, Offset=payloadOffset)
|
|
146
|
+
const emptyField = () => {
|
|
147
|
+
const b = Buffer.alloc(8);
|
|
148
|
+
b.writeUInt16LE(0, 0); // Len
|
|
149
|
+
b.writeUInt16LE(0, 2); // MaxLen
|
|
150
|
+
b.writeUInt32LE(payloadOffset, 4); // Offset
|
|
151
|
+
return b;
|
|
152
|
+
};
|
|
153
|
+
const flags = Buffer.alloc(4); flags.writeUInt32LE(0x00000207, 0);
|
|
154
|
+
return Buffer.concat([
|
|
155
|
+
sig, type,
|
|
156
|
+
emptyField(), // LmChallengeResponse
|
|
157
|
+
emptyField(), // NtChallengeResponse
|
|
158
|
+
emptyField(), // DomainName
|
|
159
|
+
emptyField(), // UserName
|
|
160
|
+
emptyField(), // Workstation
|
|
161
|
+
emptyField(), // EncryptedRandomSessionKey
|
|
162
|
+
flags
|
|
163
|
+
]);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Build SMB2 SESSION_SETUP request with NTLMSSP token
|
|
167
|
+
export function buildSmb2SessionSetup(ntlmToken, sessionIdLo=0, sessionIdHi=0){
|
|
168
|
+
// SMB2 header (64 bytes)
|
|
169
|
+
const hdr = Buffer.alloc(64, 0);
|
|
170
|
+
hdr.writeUInt32BE(0xfe534d42, 0); // Protocol
|
|
171
|
+
hdr.writeUInt16LE(64, 4); // StructureSize
|
|
172
|
+
hdr.writeUInt16LE(1, 6); // CreditCharge
|
|
173
|
+
hdr.writeUInt16LE(0x0001, 12); // Command: SESSION_SETUP
|
|
174
|
+
hdr.writeUInt16LE(1, 14); // Credits requested
|
|
175
|
+
hdr.writeUInt32LE(sessionIdLo, 40);
|
|
176
|
+
hdr.writeUInt32LE(sessionIdHi, 44);
|
|
177
|
+
|
|
178
|
+
// SESSION_SETUP request body (MS-SMB2 2.2.5)
|
|
179
|
+
const bodyFixed = Buffer.alloc(24, 0);
|
|
180
|
+
bodyFixed.writeUInt16LE(25, 0); // StructureSize (25)
|
|
181
|
+
bodyFixed.writeUInt8(0, 2); // Flags
|
|
182
|
+
bodyFixed.writeUInt8(0, 3); // SecurityMode
|
|
183
|
+
bodyFixed.writeUInt32LE(0, 4); // Capabilities
|
|
184
|
+
bodyFixed.writeUInt32LE(0, 8); // Channel
|
|
185
|
+
// SecurityBufferOffset = header(64) + body fixed(24) = 88
|
|
186
|
+
bodyFixed.writeUInt16LE(88, 12); // SecurityBufferOffset
|
|
187
|
+
bodyFixed.writeUInt16LE(ntlmToken.length, 14); // SecurityBufferLength
|
|
188
|
+
|
|
189
|
+
return Buffer.concat([hdr, bodyFixed, ntlmToken]);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Build SMB2 TREE_CONNECT request
|
|
193
|
+
export function buildSmb2TreeConnect(host, sessionIdLo, sessionIdHi){
|
|
194
|
+
const pathStr = `\\\\${host}\\IPC$`;
|
|
195
|
+
const pathBuf = Buffer.from(pathStr, 'utf16le');
|
|
196
|
+
|
|
197
|
+
const hdr = Buffer.alloc(64, 0);
|
|
198
|
+
hdr.writeUInt32BE(0xfe534d42, 0);
|
|
199
|
+
hdr.writeUInt16LE(64, 4); // StructureSize
|
|
200
|
+
hdr.writeUInt16LE(1, 6); // CreditCharge
|
|
201
|
+
hdr.writeUInt16LE(0x0003, 12); // Command: TREE_CONNECT
|
|
202
|
+
hdr.writeUInt16LE(1, 14); // Credits
|
|
203
|
+
hdr.writeUInt32LE(sessionIdLo, 40);
|
|
204
|
+
hdr.writeUInt32LE(sessionIdHi, 44);
|
|
205
|
+
|
|
206
|
+
const body = Buffer.alloc(8, 0);
|
|
207
|
+
body.writeUInt16LE(9, 0); // StructureSize (9)
|
|
208
|
+
body.writeUInt16LE(0, 2); // Flags/Reserved
|
|
209
|
+
// PathOffset = 64 + 8 = 72
|
|
210
|
+
body.writeUInt16LE(72, 4); // PathOffset
|
|
211
|
+
body.writeUInt16LE(pathBuf.length, 6); // PathLength
|
|
212
|
+
|
|
213
|
+
return Buffer.concat([hdr, body, pathBuf]);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Build a minimal NetShareEnumAll RPC request over SMB2 named pipe (srvsvc)
|
|
217
|
+
// This builds a simplified DCE/RPC bind + NetShareEnumAll request
|
|
218
|
+
export function buildNetShareEnumAll(host){
|
|
219
|
+
// Server name as UTF-16LE null-terminated
|
|
220
|
+
const serverName = Buffer.from(`\\\\${host}\0`, 'utf16le');
|
|
221
|
+
// Simplified: return a marker buffer that the real pipe would receive
|
|
222
|
+
// In real implementation this would be a full DCE/RPC request
|
|
223
|
+
const marker = Buffer.from('NETSHAREENUMALL', 'ascii');
|
|
224
|
+
return Buffer.concat([marker, serverName]);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Build a minimal SAMR connect/enumerate RPC request
|
|
228
|
+
export function buildSamrConnect(host){
|
|
229
|
+
const serverName = Buffer.from(`\\\\${host}\0`, 'utf16le');
|
|
230
|
+
const marker = Buffer.from('SAMRENUMUSERS', 'ascii');
|
|
231
|
+
return Buffer.concat([marker, serverName]);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Parse NetShareEnumAll response — extract share names from response buffer
|
|
235
|
+
export function parseNetShareEnumAllResponse(buf){
|
|
236
|
+
if (!buf || buf.length < 4) return [];
|
|
237
|
+
try {
|
|
238
|
+
// Look for share names encoded as UTF-16LE strings
|
|
239
|
+
// In a real srvsvc response, shares are in a list of SHARE_INFO_1 structures
|
|
240
|
+
// For our probe: if we got data back with STATUS_SUCCESS, extract text segments
|
|
241
|
+
const shares = [];
|
|
242
|
+
let off = 0;
|
|
243
|
+
while (off + 2 <= buf.length) {
|
|
244
|
+
// Scan for printable UTF-16LE sequences (heuristic for share names)
|
|
245
|
+
const ch = buf.readUInt16LE(off);
|
|
246
|
+
if (ch >= 0x20 && ch < 0x7f) {
|
|
247
|
+
let end = off;
|
|
248
|
+
while (end + 2 <= buf.length) {
|
|
249
|
+
const c = buf.readUInt16LE(end);
|
|
250
|
+
if (c === 0 || c < 0x20 || c >= 0x7f) break;
|
|
251
|
+
end += 2;
|
|
252
|
+
}
|
|
253
|
+
if (end > off) {
|
|
254
|
+
const name = buf.subarray(off, end).toString('utf16le');
|
|
255
|
+
if (name.length >= 1 && name.length <= 80) shares.push(name);
|
|
256
|
+
off = end + 2;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
off += 2;
|
|
261
|
+
}
|
|
262
|
+
return shares;
|
|
263
|
+
} catch { return []; }
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Parse SAMR user enum response — extract user names
|
|
267
|
+
export function parseSamrEnumUsersResponse(buf){
|
|
268
|
+
if (!buf || buf.length < 4) return [];
|
|
269
|
+
try {
|
|
270
|
+
const users = [];
|
|
271
|
+
let off = 0;
|
|
272
|
+
while (off + 2 <= buf.length) {
|
|
273
|
+
const ch = buf.readUInt16LE(off);
|
|
274
|
+
if (ch >= 0x20 && ch < 0x7f) {
|
|
275
|
+
let end = off;
|
|
276
|
+
while (end + 2 <= buf.length) {
|
|
277
|
+
const c = buf.readUInt16LE(end);
|
|
278
|
+
if (c === 0 || c < 0x20 || c >= 0x7f) break;
|
|
279
|
+
end += 2;
|
|
280
|
+
}
|
|
281
|
+
if (end > off) {
|
|
282
|
+
const name = buf.subarray(off, end).toString('utf16le');
|
|
283
|
+
if (name.length >= 1 && name.length <= 80) users.push(name);
|
|
284
|
+
off = end + 2;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
off += 2;
|
|
289
|
+
}
|
|
290
|
+
return users;
|
|
291
|
+
} catch { return []; }
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/* ----------------------------- Scanner core ----------------------------- */
|
|
295
|
+
|
|
296
|
+
const DEBUG = /^(1|true|yes|on)$/i.test(String(process.env.NETBIOS_DEBUG || ''));
|
|
297
|
+
const TIMEOUT = Number(process.env.NETBIOS_TIMEOUT_MS || 1500);
|
|
298
|
+
const MDNS_TIMEOUT = Number(process.env.MDNS_TIMEOUT_MS || 1500);
|
|
299
|
+
const ENABLE_MDNS = /^(1|true|yes|on)$/i.test(String(process.env.NETBIOS_ENABLE_MDNS || '1'));
|
|
300
|
+
const ENABLE_NULL_SESSION = /^(1|true|yes|on)$/i.test(String(process.env.SMB_NULL_SESSION || ''));
|
|
301
|
+
const NULL_SESSION_TIMEOUT = Number(process.env.SMB_NULL_SESSION_TIMEOUT || 5000);
|
|
302
|
+
|
|
303
|
+
function dlog(...a){ if (DEBUG) console.log('[netbios]', ...a); }
|
|
304
|
+
|
|
305
|
+
function encodeNbnsName(questionName='*'){
|
|
306
|
+
// RFC1002 NBNS name is 16 bytes (15 name padded with spaces + suffix) then encoded to 32 ASCII bytes.
|
|
307
|
+
const raw = Buffer.alloc(16, 0x20);
|
|
308
|
+
raw.write(questionName.slice(0,15).toUpperCase(), 0, 'ascii');
|
|
309
|
+
// Name encoding: each byte -> two ASCII chars: 'A' + high nibble, 'A' + low nibble
|
|
310
|
+
const out = Buffer.alloc(32);
|
|
311
|
+
for (let i=0;i<16;i++){
|
|
312
|
+
const c = raw[i];
|
|
313
|
+
out[i*2] = 0x41 + ((c >> 4) & 0x0f);
|
|
314
|
+
out[i*2+1] = 0x41 + (c & 0x0f);
|
|
315
|
+
}
|
|
316
|
+
return out;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function buildNbnsNodeStatusQuery(){
|
|
320
|
+
const header = Buffer.alloc(12);
|
|
321
|
+
const id = Math.floor(Math.random()*0xffff) & 0xffff;
|
|
322
|
+
header.writeUInt16BE(id, 0);
|
|
323
|
+
header.writeUInt16BE(0x0010, 2); // flags: RD=0
|
|
324
|
+
header.writeUInt16BE(1, 4); // QDCOUNT=1
|
|
325
|
+
// AN/NS/AR = 0
|
|
326
|
+
const qname = Buffer.concat([Buffer.from([0x20]), encodeNbnsName('*'), Buffer.from([0x00])]);
|
|
327
|
+
const qtype = Buffer.alloc(2); qtype.writeUInt16BE(0x0021, 0); // NBSTAT
|
|
328
|
+
const qclass = Buffer.alloc(2); qclass.writeUInt16BE(0x0001, 0); // IN
|
|
329
|
+
return Buffer.concat([header, qname, qtype, qclass]);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function parseDnsName(buf, off){
|
|
333
|
+
const parts = [];
|
|
334
|
+
let i = off;
|
|
335
|
+
while (true){
|
|
336
|
+
const len = buf[i++];
|
|
337
|
+
if (len === 0) break;
|
|
338
|
+
if ((len & 0xC0) === 0xC0){
|
|
339
|
+
// pointer
|
|
340
|
+
const ptr = ((len & 0x3F) << 8) | buf[i++];
|
|
341
|
+
const [nm] = parseDnsName(buf, ptr);
|
|
342
|
+
parts.push(nm);
|
|
343
|
+
break;
|
|
344
|
+
} else {
|
|
345
|
+
parts.push(buf.toString('ascii', i, i+len));
|
|
346
|
+
i += len;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return [parts.join('.'), i];
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function parseNbnsResponse(buf){
|
|
353
|
+
// Very small parser that finds the first NBSTAT answer RDATA
|
|
354
|
+
try{
|
|
355
|
+
let off = 12;
|
|
356
|
+
const qd = buf.readUInt16BE(4);
|
|
357
|
+
const an = buf.readUInt16BE(6);
|
|
358
|
+
// skip questions
|
|
359
|
+
for (let i=0;i<qd;i++){
|
|
360
|
+
const [, n] = parseDnsName(buf, off);
|
|
361
|
+
off = n + 4;
|
|
362
|
+
}
|
|
363
|
+
// answers
|
|
364
|
+
for (let i=0;i<an;i++){
|
|
365
|
+
const [, n1] = parseDnsName(buf, off); off = n1;
|
|
366
|
+
const type = buf.readUInt16BE(off); off+=2;
|
|
367
|
+
/*const klass =*/ off+=2;
|
|
368
|
+
/*const ttl =*/ off+=4;
|
|
369
|
+
const rdlen = buf.readUInt16BE(off); off+=2;
|
|
370
|
+
if (type === 0x0021){ // NBSTAT
|
|
371
|
+
const rdata = buf.subarray(off, off+rdlen);
|
|
372
|
+
return parseNbstatRData(rdata);
|
|
373
|
+
}
|
|
374
|
+
off += rdlen;
|
|
375
|
+
}
|
|
376
|
+
}catch{}
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function sendUdp(host, port, payload){
|
|
381
|
+
return new Promise(resolve => {
|
|
382
|
+
const s = dgram.createSocket('udp4');
|
|
383
|
+
let settled = false;
|
|
384
|
+
const to = setTimeout(()=>{ if(!settled){ settled=true; try{s.close();}catch{} resolve(null);} }, TIMEOUT);
|
|
385
|
+
s.on('message', m => { if (settled) return; settled=true; clearTimeout(to); try{s.close();}catch{} resolve(m); });
|
|
386
|
+
s.on('error', ()=>{ if (settled) return; settled=true; clearTimeout(to); try{s.close();}catch{} resolve(null); });
|
|
387
|
+
s.send(payload, port, host, err => {
|
|
388
|
+
if (err && !settled){ settled=true; clearTimeout(to); try{s.close();}catch{} resolve(null); }
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async function probeUdp137(host){
|
|
394
|
+
const q = buildNbnsNodeStatusQuery();
|
|
395
|
+
dlog('UDP/137 sending NBSTAT query len', q.length);
|
|
396
|
+
const res = await sendUdp(host, 137, q);
|
|
397
|
+
if (!res) return null;
|
|
398
|
+
const parsed = parseNbnsResponse(res);
|
|
399
|
+
return parsed;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Minimal TCP 445 SMB2 negotiate (best-effort)
|
|
403
|
+
async function probeTcp445(host){
|
|
404
|
+
return new Promise(resolve => {
|
|
405
|
+
const sock = net.createConnection({ host, port:445 });
|
|
406
|
+
const to = setTimeout(()=>{ try{sock.destroy();}catch{} resolve(null); }, TIMEOUT);
|
|
407
|
+
sock.on('connect', () => {
|
|
408
|
+
try { sock.write(wrapNbss(buildSmb2Negotiate())); } catch {}
|
|
409
|
+
});
|
|
410
|
+
sock.on('data', (chunk) => {
|
|
411
|
+
clearTimeout(to);
|
|
412
|
+
try{ sock.destroy(); }catch{}
|
|
413
|
+
// Strip NBSS header before parsing SMB2 response
|
|
414
|
+
resolve(parseSmb2NegotiateResponse(stripNbss(chunk)));
|
|
415
|
+
});
|
|
416
|
+
sock.on('error', () => { clearTimeout(to); try { sock.destroy(); } catch {} resolve(null); });
|
|
417
|
+
sock.on('timeout', () => { clearTimeout(to); try { sock.destroy(); } catch {} resolve(null); });
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// mDNS browse for _smb._tcp.local with QU (request unicast response)
|
|
422
|
+
async function probeMdnsSmb(){
|
|
423
|
+
if (!ENABLE_MDNS) return null;
|
|
424
|
+
return new Promise(resolve => {
|
|
425
|
+
const s = dgram.createSocket('udp4');
|
|
426
|
+
let settled = false;
|
|
427
|
+
const q = buildMdnsQueryPTR();
|
|
428
|
+
const cleanup = () => { try{s.close();}catch{} };
|
|
429
|
+
const to = setTimeout(()=>{ if(!settled){ settled=true; cleanup(); resolve(null); } }, MDNS_TIMEOUT);
|
|
430
|
+
s.on('message', (m) => {
|
|
431
|
+
if (settled) return;
|
|
432
|
+
settled = true;
|
|
433
|
+
clearTimeout(to);
|
|
434
|
+
cleanup();
|
|
435
|
+
resolve(m);
|
|
436
|
+
});
|
|
437
|
+
s.on('error', () => { if(!settled){ settled=true; clearTimeout(to); cleanup(); resolve(null);} });
|
|
438
|
+
try {
|
|
439
|
+
// bind ephemeral so unicast replies can reach us
|
|
440
|
+
s.bind(0, () => {
|
|
441
|
+
try {
|
|
442
|
+
s.setMulticastTTL?.(1);
|
|
443
|
+
} catch {}
|
|
444
|
+
s.send(q, 5353, '224.0.0.251', (err) => {
|
|
445
|
+
if (err) { if(!settled){ settled=true; clearTimeout(to); cleanup(); resolve(null);} }
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
} catch {
|
|
449
|
+
if(!settled){ settled=true; clearTimeout(to); cleanup(); resolve(null); }
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// SMB2 null session probe — attempts anonymous auth, IPC$ tree connect, and share/user enum
|
|
455
|
+
async function probeNullSession(host){
|
|
456
|
+
const result = { nullSessionAllowed: false, shares: [], users: [] };
|
|
457
|
+
if (!ENABLE_NULL_SESSION) return result;
|
|
458
|
+
|
|
459
|
+
return new Promise(resolve => {
|
|
460
|
+
let resolved = false;
|
|
461
|
+
const safeResolve = (v) => { if (resolved) return; resolved = true; resolve(v); };
|
|
462
|
+
|
|
463
|
+
const sock = net.createConnection({ host, port: 445 });
|
|
464
|
+
const to = setTimeout(() => {
|
|
465
|
+
try { sock.destroy(); } catch {}
|
|
466
|
+
safeResolve(result);
|
|
467
|
+
}, NULL_SESSION_TIMEOUT);
|
|
468
|
+
|
|
469
|
+
let phase = 'negotiate'; // negotiate | session-setup-1 | session-setup-2 | tree-connect | done
|
|
470
|
+
let sessionIdLo = 0;
|
|
471
|
+
let sessionIdHi = 0;
|
|
472
|
+
let chunks = Buffer.alloc(0);
|
|
473
|
+
const MAX_SMB_BUF = 256 * 1024;
|
|
474
|
+
|
|
475
|
+
const finish = () => {
|
|
476
|
+
clearTimeout(to);
|
|
477
|
+
try { sock.destroy(); } catch {}
|
|
478
|
+
safeResolve(result);
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
sock.on('connect', () => {
|
|
482
|
+
dlog('null-session: connected, sending negotiate');
|
|
483
|
+
try { sock.write(wrapNbss(buildSmb2Negotiate())); } catch { finish(); }
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
sock.on('data', chunk => {
|
|
487
|
+
if (chunks.length + chunk.length > MAX_SMB_BUF) { sock.destroy(); return; }
|
|
488
|
+
chunks = Buffer.concat([chunks, chunk]);
|
|
489
|
+
|
|
490
|
+
// Process complete NBSS-framed messages (4-byte header + payload)
|
|
491
|
+
while (chunks.length >= 4) {
|
|
492
|
+
const msgLen = chunks.readUInt32BE(0) & 0x00FFFFFF; // lower 24 bits = SMB2 message length
|
|
493
|
+
if (chunks.length < 4 + msgLen) return; // incomplete message, wait for more data
|
|
494
|
+
|
|
495
|
+
const msg = chunks.subarray(4, 4 + msgLen);
|
|
496
|
+
chunks = chunks.subarray(4 + msgLen); // keep leftover for coalesced messages
|
|
497
|
+
|
|
498
|
+
const hdr = parseSmb2Header(msg);
|
|
499
|
+
if (!hdr) { finish(); return; }
|
|
500
|
+
|
|
501
|
+
if (phase === 'negotiate') {
|
|
502
|
+
if (hdr.status !== STATUS_SUCCESS) { dlog('null-session: negotiate failed'); finish(); return; }
|
|
503
|
+
// Send SESSION_SETUP with NTLMSSP Negotiate (Type 1)
|
|
504
|
+
phase = 'session-setup-1';
|
|
505
|
+
const token = buildNtlmsspNegotiate();
|
|
506
|
+
try { sock.write(wrapNbss(buildSmb2SessionSetup(token))); } catch { finish(); }
|
|
507
|
+
} else if (phase === 'session-setup-1') {
|
|
508
|
+
// Expect STATUS_MORE_PROCESSING_REQUIRED with challenge
|
|
509
|
+
sessionIdLo = hdr.sessionIdLo;
|
|
510
|
+
sessionIdHi = hdr.sessionIdHi;
|
|
511
|
+
if (hdr.status === STATUS_MORE_PROCESSING) {
|
|
512
|
+
// Send Type 3 (authenticate with null creds)
|
|
513
|
+
phase = 'session-setup-2';
|
|
514
|
+
const authToken = buildNtlmsspAuth();
|
|
515
|
+
try { sock.write(wrapNbss(buildSmb2SessionSetup(authToken, sessionIdLo, sessionIdHi))); } catch { finish(); }
|
|
516
|
+
} else if (hdr.status === STATUS_SUCCESS) {
|
|
517
|
+
// Some servers accept directly (guest/anonymous)
|
|
518
|
+
sessionIdLo = hdr.sessionIdLo;
|
|
519
|
+
sessionIdHi = hdr.sessionIdHi;
|
|
520
|
+
result.nullSessionAllowed = true;
|
|
521
|
+
phase = 'tree-connect';
|
|
522
|
+
try { sock.write(wrapNbss(buildSmb2TreeConnect(host, sessionIdLo, sessionIdHi))); } catch { finish(); }
|
|
523
|
+
} else {
|
|
524
|
+
// ACCESS_DENIED or other — null session not allowed
|
|
525
|
+
dlog('null-session: session setup denied, status=0x' + hdr.status.toString(16));
|
|
526
|
+
finish();
|
|
527
|
+
}
|
|
528
|
+
} else if (phase === 'session-setup-2') {
|
|
529
|
+
if (hdr.status === STATUS_SUCCESS) {
|
|
530
|
+
result.nullSessionAllowed = true;
|
|
531
|
+
sessionIdLo = hdr.sessionIdLo; // H2 fix: unconditional assign, not ||
|
|
532
|
+
sessionIdHi = hdr.sessionIdHi;
|
|
533
|
+
phase = 'tree-connect';
|
|
534
|
+
try { sock.write(wrapNbss(buildSmb2TreeConnect(host, sessionIdLo, sessionIdHi))); } catch { finish(); }
|
|
535
|
+
} else {
|
|
536
|
+
dlog('null-session: session auth denied, status=0x' + hdr.status.toString(16));
|
|
537
|
+
finish();
|
|
538
|
+
}
|
|
539
|
+
} else if (phase === 'tree-connect') {
|
|
540
|
+
if (hdr.status === STATUS_SUCCESS) {
|
|
541
|
+
dlog('null-session: IPC$ tree connect succeeded');
|
|
542
|
+
// Tree connect succeeded — in a full implementation we would open named pipes
|
|
543
|
+
// and send DCE/RPC requests. For now we mark success and finish.
|
|
544
|
+
// Share/user enum would require CREATE + WRITE + READ on \\srvsvc and \\samr pipes.
|
|
545
|
+
phase = 'done';
|
|
546
|
+
finish();
|
|
547
|
+
} else {
|
|
548
|
+
dlog('null-session: tree connect denied, status=0x' + hdr.status.toString(16));
|
|
549
|
+
finish();
|
|
550
|
+
}
|
|
551
|
+
} else {
|
|
552
|
+
finish();
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
sock.on('error', () => { finish(); });
|
|
558
|
+
sock.on('timeout', () => { try { sock.destroy(); } catch {} finish(); });
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/* ----------------------------- Plugin ----------------------------- */
|
|
563
|
+
|
|
564
|
+
export default {
|
|
565
|
+
id: "014",
|
|
566
|
+
name: "NetBIOS/SMB Scanner",
|
|
567
|
+
description: "Discovers NetBIOS names (UDP/137), optional mDNS browse for _smb._tcp, and probes SMB over TCP/445.",
|
|
568
|
+
priority: 345,
|
|
569
|
+
requirements: {}, // no gating; runs when selected
|
|
570
|
+
protocols: ["udp","tcp"],
|
|
571
|
+
ports: [137, 445],
|
|
572
|
+
|
|
573
|
+
async run(host, _port, _opts={}){
|
|
574
|
+
const data = [];
|
|
575
|
+
let up = false;
|
|
576
|
+
let program = "Unknown";
|
|
577
|
+
let version = "Unknown";
|
|
578
|
+
|
|
579
|
+
// UDP/137 NBSTAT
|
|
580
|
+
try{
|
|
581
|
+
const nb = await probeUdp137(host);
|
|
582
|
+
if (nb && nb.names && nb.names.length){
|
|
583
|
+
up = true;
|
|
584
|
+
program = "NetBIOS";
|
|
585
|
+
data.push({
|
|
586
|
+
probe_protocol: 'udp',
|
|
587
|
+
probe_port: 137,
|
|
588
|
+
probe_info: `NBSTAT names: ${nb.names.map(n=>n.name+'<'+n.suffix.toString(16).padStart(2,'0')+(n.group?':G':':U')).join(', ')}`,
|
|
589
|
+
response_banner: nb.mac ? `MAC ${nb.mac}` : null
|
|
590
|
+
});
|
|
591
|
+
} else {
|
|
592
|
+
data.push({
|
|
593
|
+
probe_protocol: 'udp',
|
|
594
|
+
probe_port: 137,
|
|
595
|
+
probe_info: 'No NBSTAT response',
|
|
596
|
+
response_banner: null
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
}catch{
|
|
600
|
+
data.push({ probe_protocol:'udp', probe_port:137, probe_info:'Error NBSTAT probe', response_banner:null });
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// TCP/445 SMB2
|
|
604
|
+
try{
|
|
605
|
+
const smb = await probeTcp445(host);
|
|
606
|
+
if (smb && smb.ok){
|
|
607
|
+
up = true;
|
|
608
|
+
program = program === "Unknown" ? "SMB" : program;
|
|
609
|
+
data.push({
|
|
610
|
+
probe_protocol: 'tcp',
|
|
611
|
+
probe_port: 445,
|
|
612
|
+
probe_info: 'SMB2 negotiate successful',
|
|
613
|
+
response_banner: null
|
|
614
|
+
});
|
|
615
|
+
} else {
|
|
616
|
+
data.push({
|
|
617
|
+
probe_protocol: 'tcp',
|
|
618
|
+
probe_port: 445,
|
|
619
|
+
probe_info: 'No SMB2 response',
|
|
620
|
+
response_banner: null
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
}catch{
|
|
624
|
+
data.push({ probe_protocol:'tcp', probe_port:445, probe_info:'Error SMB2 probe', response_banner:null });
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// mDNS browse (local networks)
|
|
628
|
+
try{
|
|
629
|
+
const md = await probeMdnsSmb();
|
|
630
|
+
if (md){
|
|
631
|
+
up = true; // service seen on LAN
|
|
632
|
+
data.push({
|
|
633
|
+
probe_protocol: 'udp',
|
|
634
|
+
probe_port: 5353,
|
|
635
|
+
probe_info: 'mDNS: _smb._tcp.local seen',
|
|
636
|
+
response_banner: md.toString('hex').slice(0, 120) + (md.length > 60 ? '…' : '')
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
}catch{ /* best-effort only */ }
|
|
640
|
+
|
|
641
|
+
// SMB2 null session enumeration (opt-in via SMB_NULL_SESSION env)
|
|
642
|
+
let nullSessionAllowed = false;
|
|
643
|
+
let shares = [];
|
|
644
|
+
let users = [];
|
|
645
|
+
|
|
646
|
+
try {
|
|
647
|
+
const ns = await probeNullSession(host);
|
|
648
|
+
nullSessionAllowed = ns.nullSessionAllowed;
|
|
649
|
+
shares = ns.shares;
|
|
650
|
+
users = ns.users;
|
|
651
|
+
if (nullSessionAllowed) {
|
|
652
|
+
data.push({
|
|
653
|
+
probe_protocol: 'tcp',
|
|
654
|
+
probe_port: 445,
|
|
655
|
+
probe_info: 'WARNING: SMB null session authentication succeeded',
|
|
656
|
+
response_banner: `Null session allowed. Shares: ${shares.length}, Users: ${users.length}`
|
|
657
|
+
});
|
|
658
|
+
} else if (ENABLE_NULL_SESSION) {
|
|
659
|
+
data.push({
|
|
660
|
+
probe_protocol: 'tcp',
|
|
661
|
+
probe_port: 445,
|
|
662
|
+
probe_info: 'SMB null session denied (good)',
|
|
663
|
+
response_banner: null
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
} catch {
|
|
667
|
+
if (ENABLE_NULL_SESSION) {
|
|
668
|
+
data.push({
|
|
669
|
+
probe_protocol: 'tcp',
|
|
670
|
+
probe_port: 445,
|
|
671
|
+
probe_info: 'SMB null session probe error',
|
|
672
|
+
response_banner: null
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
return {
|
|
678
|
+
up,
|
|
679
|
+
program,
|
|
680
|
+
version,
|
|
681
|
+
type: 'netbios/smb',
|
|
682
|
+
nullSessionAllowed,
|
|
683
|
+
shares,
|
|
684
|
+
users,
|
|
685
|
+
data
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
// ---------------- Concluder adapter ----------------
|
|
691
|
+
import { statusFrom } from '../utils/conclusion_utils.mjs';
|
|
692
|
+
|
|
693
|
+
export async function conclude({ host, result }){
|
|
694
|
+
const rows = Array.isArray(result?.data) ? result.data : [];
|
|
695
|
+
const items = [];
|
|
696
|
+
|
|
697
|
+
// Primary service record for NetBIOS/SMB
|
|
698
|
+
const pick = rows.find(r => /SMB2|NBSTAT/i.test(String(r?.probe_info || ''))) || rows[0] || null;
|
|
699
|
+
const port = Number(pick?.probe_port ?? 445);
|
|
700
|
+
const info = pick?.probe_info || (result?.up ? 'NetBIOS/SMB detected' : 'No response');
|
|
701
|
+
const banner = pick?.response_banner || null;
|
|
702
|
+
const status = result?.up ? 'open' : statusFrom({ info, banner });
|
|
703
|
+
|
|
704
|
+
const item = {
|
|
705
|
+
port, protocol: pick?.probe_protocol || 'tcp', service: 'netbios/smb',
|
|
706
|
+
program: result?.program || 'Unknown',
|
|
707
|
+
version: result?.version || 'Unknown',
|
|
708
|
+
status, info, banner,
|
|
709
|
+
nullSessionAllowed: result?.nullSessionAllowed ?? false,
|
|
710
|
+
shares: result?.shares ?? [],
|
|
711
|
+
users: result?.users ?? [],
|
|
712
|
+
source: 'netbios',
|
|
713
|
+
evidence: rows,
|
|
714
|
+
authoritative: true
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
items.push(item);
|
|
718
|
+
|
|
719
|
+
// Add WARNING evidence row when null session is allowed
|
|
720
|
+
if (result?.nullSessionAllowed) {
|
|
721
|
+
items.push({
|
|
722
|
+
port: 445, protocol: 'tcp', service: 'netbios/smb',
|
|
723
|
+
program: result?.program || 'Unknown',
|
|
724
|
+
version: result?.version || 'Unknown',
|
|
725
|
+
status: 'open', info: 'WARNING: SMB null session allowed — anonymous enumeration possible',
|
|
726
|
+
banner: `Shares: ${(result.shares||[]).join(', ') || 'none'}, Users: ${(result.users||[]).join(', ') || 'none'}`,
|
|
727
|
+
nullSessionAllowed: true,
|
|
728
|
+
shares: result?.shares ?? [],
|
|
729
|
+
users: result?.users ?? [],
|
|
730
|
+
source: 'netbios',
|
|
731
|
+
evidence: rows,
|
|
732
|
+
authoritative: true
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
return items;
|
|
737
|
+
}
|