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,162 @@
|
|
|
1
|
+
// plugins/arp_scanner.mjs
|
|
2
|
+
// ARP Scanner — infers vendor/OS hints from ARP (local targets only) via ctx helpers.
|
|
3
|
+
// Short-circuits if a prior plugin already inferred OS (e.g., Ping Checker or others).
|
|
4
|
+
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { execFile } from "node:child_process";
|
|
7
|
+
import { isPrivateLike } from '../utils/net_validation.mjs';
|
|
8
|
+
|
|
9
|
+
const execFileP = promisify(execFile);
|
|
10
|
+
|
|
11
|
+
const DEBUG = /^(1|true|yes|on)$/i.test(String(process.env.DEBUG_MODE || process.env.ARP_DEBUG || ""));
|
|
12
|
+
const ONLY_IF_OS_UNKNOWN = !/^(0|false|no|off)$/i.test(String(process.env.ARP_RUN_ONLY_IF_OS_UNKNOWN ?? "1"));
|
|
13
|
+
|
|
14
|
+
function dlog(...a) { if (DEBUG) console.log("[arp-scanner]", ...a); }
|
|
15
|
+
|
|
16
|
+
// --- Exported: tests expect a STRING MAC (or null) ---
|
|
17
|
+
export function parseArpOutput(out, ip) {
|
|
18
|
+
if (!out) return null;
|
|
19
|
+
|
|
20
|
+
// Normalize MAC patterns (colon or dash, 1 or 2 hex digits per octet)
|
|
21
|
+
const macRe = /([0-9a-f]{1,2}(?:[:-][0-9a-f]{1,2}){5})/i;
|
|
22
|
+
|
|
23
|
+
// Log raw output for debugging
|
|
24
|
+
dlog("Raw ARP output:", out);
|
|
25
|
+
|
|
26
|
+
// Prefer a line with the specific IP if provided (macOS/Linux: IP in parentheses or first column; Windows: "Internet Address")
|
|
27
|
+
if (ip) {
|
|
28
|
+
const ipLine = out
|
|
29
|
+
.split(/\r?\n/)
|
|
30
|
+
.find((l) => l.includes(ip) || l.includes(`(${ip})`));
|
|
31
|
+
if (ipLine) {
|
|
32
|
+
const m = ipLine.match(macRe);
|
|
33
|
+
if (m) {
|
|
34
|
+
let mac = m[1].replace(/-/g, ":").toUpperCase();
|
|
35
|
+
// Normalize to two digits per octet
|
|
36
|
+
mac = mac.split(":").map(part => part.padStart(2, "0")).join(":");
|
|
37
|
+
dlog(`Parsed MAC for IP ${ip}: ${mac}`);
|
|
38
|
+
return mac;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Fallback: first MAC in the whole output
|
|
44
|
+
const m2 = out.match(macRe);
|
|
45
|
+
if (m2) {
|
|
46
|
+
let mac = m2[1].replace(/-/g, ":").toUpperCase();
|
|
47
|
+
mac = mac.split(":").map(part => part.padStart(2, "0")).join(":");
|
|
48
|
+
dlog(`Parsed fallback MAC: ${mac}`);
|
|
49
|
+
return mac;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
dlog("No MAC found in output");
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function getMacViaArp(ip, iface = null) {
|
|
57
|
+
const cmd = "arp";
|
|
58
|
+
const args = process.platform === "win32" ? ["-a", ip] : ["-n", ip];
|
|
59
|
+
// Add interface specification for macOS/Linux if provided
|
|
60
|
+
if (iface && process.platform !== "win32") {
|
|
61
|
+
args.push("-i", iface);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const { stdout } = await execFileP(cmd, args, { windowsHide: true, timeout: 5000 });
|
|
66
|
+
return parseArpOutput(stdout, ip);
|
|
67
|
+
} catch (e) {
|
|
68
|
+
dlog("arp exec failed:", e?.message || e);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export default {
|
|
74
|
+
id: "026",
|
|
75
|
+
name: "ARP Scanner",
|
|
76
|
+
description: "Infers vendor/OS hints from ARP (local targets only) via ctx helpers.",
|
|
77
|
+
priority: 25,
|
|
78
|
+
requirements: {},
|
|
79
|
+
protocols: ["arp"],
|
|
80
|
+
ports: [],
|
|
81
|
+
|
|
82
|
+
async run(host, _port, opts = {}) {
|
|
83
|
+
const data = [];
|
|
84
|
+
let up = false;
|
|
85
|
+
let os = null;
|
|
86
|
+
|
|
87
|
+
// ----- Short-circuit logic (no manager changes required) -----
|
|
88
|
+
const ctx = opts?.context || {};
|
|
89
|
+
const osKnown = !!ctx.os || !!ctx.arpOs || !!ctx.pingOs;
|
|
90
|
+
if (ONLY_IF_OS_UNKNOWN && osKnown) {
|
|
91
|
+
dlog("Skipping ARP scan — OS already known by prior probe");
|
|
92
|
+
data.push({
|
|
93
|
+
probe_protocol: "arp",
|
|
94
|
+
probe_port: 0,
|
|
95
|
+
probe_info: "Skipped: OS already known from prior plugin",
|
|
96
|
+
response_banner: null
|
|
97
|
+
});
|
|
98
|
+
return { up: false, os: null, data };
|
|
99
|
+
}
|
|
100
|
+
// -------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
// Only meaningful on local subnets
|
|
103
|
+
if (!isPrivateLike(host)) {
|
|
104
|
+
dlog("Target not in private/local range; ARP not attempted.");
|
|
105
|
+
data.push({
|
|
106
|
+
probe_protocol: "arp",
|
|
107
|
+
probe_port: 0,
|
|
108
|
+
probe_info: "Non-local target — ARP not attempted",
|
|
109
|
+
response_banner: null
|
|
110
|
+
});
|
|
111
|
+
return { up: false, os: null, data };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Try with interface 'en0' for macOS, fallback to no interface
|
|
115
|
+
const interfaces = process.platform === "win32" ? [null] : [null, "en0"];
|
|
116
|
+
let mac = null;
|
|
117
|
+
for (const iface of interfaces) {
|
|
118
|
+
mac = await getMacViaArp(host, iface);
|
|
119
|
+
if (mac) break;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!mac) {
|
|
123
|
+
data.push({
|
|
124
|
+
probe_protocol: "arp",
|
|
125
|
+
probe_port: 0,
|
|
126
|
+
probe_info: "No ARP entry",
|
|
127
|
+
response_banner: null,
|
|
128
|
+
mac: null
|
|
129
|
+
});
|
|
130
|
+
return { up: false, os: null, data };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
up = true; // ARP implies L2 presence
|
|
134
|
+
|
|
135
|
+
// Use ctx helpers only (no internal OUI loading)
|
|
136
|
+
const vendorRaw = typeof ctx.lookupVendor === "function" ? (ctx.lookupVendor(mac) || null) : null;
|
|
137
|
+
const vendor = vendorRaw ? vendorRaw.replace(/[\r\n]+/g, ' ').trim() : null;
|
|
138
|
+
const info = vendor ? `ARP entry found — vendor: ${vendor}` : "ARP entry found";
|
|
139
|
+
data.push({
|
|
140
|
+
probe_protocol: "arp",
|
|
141
|
+
probe_port: 0,
|
|
142
|
+
probe_info: info,
|
|
143
|
+
response_banner: mac,
|
|
144
|
+
mac
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Vendor → OS heuristic via ctx helper (conservative)
|
|
148
|
+
if (typeof ctx.probableOsFromVendor === "function") {
|
|
149
|
+
const vendorOs = ctx.probableOsFromVendor(vendor);
|
|
150
|
+
if (vendorOs && vendorOs !== "Unknown") os = vendorOs;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
up,
|
|
155
|
+
program: "ARP",
|
|
156
|
+
version: "Unknown",
|
|
157
|
+
os,
|
|
158
|
+
type: "arp",
|
|
159
|
+
data
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
};
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// plugins/db_scanner.mjs
|
|
2
|
+
// Lightweight DB product/version probes (no external deps). Best-effort banner grabs.
|
|
3
|
+
//
|
|
4
|
+
// Detects: MySQL/MariaDB (3306), Microsoft SQL Server (1433), PostgreSQL (5432),
|
|
5
|
+
// Oracle TNS (1521), MongoDB (27017)
|
|
6
|
+
|
|
7
|
+
import net from 'node:net';
|
|
8
|
+
|
|
9
|
+
const TIMEOUT = 2500;
|
|
10
|
+
const DB_PORTS = [3306, 1433, 5432, 1521, 27017];
|
|
11
|
+
|
|
12
|
+
function connect(host, port, timeoutMs) {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
const s = new net.Socket();
|
|
15
|
+
let done = false;
|
|
16
|
+
const finish = (err, sock) => { if (done) return; done = true; err ? reject(err) : resolve(sock); };
|
|
17
|
+
s.setNoDelay(true);
|
|
18
|
+
s.setTimeout(timeoutMs, () => { try{s.destroy()}catch{}; finish(new Error('timeout')); });
|
|
19
|
+
s.once('error', (e)=>{ try{s.destroy()}catch{}; finish(e); });
|
|
20
|
+
s.connect(port, host, ()=>finish(null, s));
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const readAll = (sock, ms) => new Promise(res => {
|
|
25
|
+
const chunks = []; let len = 0; const t = setTimeout(() => end(), ms);
|
|
26
|
+
const end = () => { clearTimeout(t); try{sock.destroy()}catch{}; res(Buffer.concat(chunks, len)); };
|
|
27
|
+
sock.on('data', b => { chunks.push(b); len += b.length; if (len > 65536) end(); });
|
|
28
|
+
sock.on('end', end); sock.on('close', end); sock.on('error', end);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// --- MySQL / MariaDB (3306) ---
|
|
32
|
+
async function probeMySQL(host, port, timeoutMs) {
|
|
33
|
+
const s = await connect(host, port, timeoutMs);
|
|
34
|
+
const buf = await readAll(s, timeoutMs); // server greets first
|
|
35
|
+
if (buf.length >= 6 && buf[4] === 0x0a) {
|
|
36
|
+
let i = 5;
|
|
37
|
+
while (i < buf.length && buf[i] !== 0x00) i++;
|
|
38
|
+
const verStr = buf.slice(5, i).toString('utf8');
|
|
39
|
+
const lower = verStr.toLowerCase();
|
|
40
|
+
const product = lower.includes('mariadb') ? 'MariaDB' : 'MySQL';
|
|
41
|
+
const m = verStr.match(/\d+\.\d+(?:\.\d+)?/);
|
|
42
|
+
return { type: 'mysql', product, version: m ? m[0] : '', banner: `MySQL greeting: ${verStr}` };
|
|
43
|
+
}
|
|
44
|
+
return { type: 'mysql', product: 'MySQL', version: '', banner: 'MySQL (no greeting parsed)' };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// --- Microsoft SQL Server / TDS (1433) ---
|
|
48
|
+
async function probeMSSQL(host, port, timeoutMs) {
|
|
49
|
+
const s = await connect(host, port, timeoutMs);
|
|
50
|
+
// Minimal PRELOGIN request (TDS 7.x)
|
|
51
|
+
const prelogin = Buffer.from([
|
|
52
|
+
0x12,0x01,0x00,0x14,0x00,0x00,0x00,0x00, // TDS header
|
|
53
|
+
0x00,0x00,0x06,0x00,0x06, // VERSION token, offset 0x0006, len 6
|
|
54
|
+
0xFF, // terminator
|
|
55
|
+
0x08,0x00,0x01,0x55,0x00,0x00 // arbitrary client version payload
|
|
56
|
+
]);
|
|
57
|
+
s.write(prelogin);
|
|
58
|
+
const buf = await readAll(s, timeoutMs);
|
|
59
|
+
const hdrLen = 8;
|
|
60
|
+
if (buf.length <= hdrLen) {
|
|
61
|
+
return { type: 'mssql', product: 'Microsoft SQL Server', version: '', banner: 'TDS (no response)' };
|
|
62
|
+
}
|
|
63
|
+
const payload = buf.slice(hdrLen);
|
|
64
|
+
let i = 0, version = '';
|
|
65
|
+
while (i + 4 < payload.length && payload[i] !== 0xFF) {
|
|
66
|
+
const token = payload[i];
|
|
67
|
+
const offset = payload.readUInt16BE(i+1);
|
|
68
|
+
const length = payload.readUInt16BE(i+3);
|
|
69
|
+
if (token === 0x00 && offset + length <= payload.length && length === 6) {
|
|
70
|
+
const v = payload.slice(offset, offset + length);
|
|
71
|
+
const major = v[0], minor = v[1], build = v.readUInt16BE(2), sub = v.readUInt16BE(4);
|
|
72
|
+
version = `${major}.${minor}.${build}.${sub}`;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
i += 5;
|
|
76
|
+
}
|
|
77
|
+
const map = { 16: 'SQL Server 2022', 15: 'SQL Server 2019', 14: 'SQL Server 2017', 13: 'SQL Server 2016', 12: 'SQL Server 2014', 11: 'SQL Server 2012', 10: 'SQL Server 2008/R2', 9: 'SQL Server 2005', 8: 'SQL Server 2000' };
|
|
78
|
+
const edition = version ? map[Number(version.split('.')[0])] || '' : '';
|
|
79
|
+
return {
|
|
80
|
+
type: 'mssql',
|
|
81
|
+
product: 'Microsoft SQL Server',
|
|
82
|
+
version,
|
|
83
|
+
banner: version ? `MSSQL PRELOGIN: ${version}${edition ? ' ('+edition+')' : ''}` : 'MSSQL (no version parsed)'
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// --- PostgreSQL (5432) ---
|
|
88
|
+
async function probePostgres(host, port, timeoutMs) {
|
|
89
|
+
const s = await connect(host, port, timeoutMs);
|
|
90
|
+
// StartupMessage: len(4) + protocol 3.0 (4) + params (key\0val\0... \0)
|
|
91
|
+
const params = Buffer.from('user\0pgscan\0database\0postgres\0application_name\0audit-scan\0\0','utf8');
|
|
92
|
+
const startup = Buffer.alloc(8 + params.length);
|
|
93
|
+
startup.writeUInt32BE(startup.length, 0);
|
|
94
|
+
startup.writeUInt32BE(196608, 4); // 3.0
|
|
95
|
+
params.copy(startup, 8);
|
|
96
|
+
s.write(startup);
|
|
97
|
+
const buf = await readAll(s, timeoutMs);
|
|
98
|
+
const text = buf.toString('utf8');
|
|
99
|
+
const m = text.match(/PostgreSQL\s+(\d+\.\d+(?:\.\d+)?)/i);
|
|
100
|
+
const version = m ? m[1] : '';
|
|
101
|
+
return { type: 'postgresql', product: 'PostgreSQL', version, banner: version ? `PostgreSQL ${version}` : 'PostgreSQL (version not disclosed)' };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// --- Oracle TNS (1521) — heuristic ---
|
|
105
|
+
async function probeOracleTNS(host, port, timeoutMs) {
|
|
106
|
+
const s = await connect(host, port, timeoutMs);
|
|
107
|
+
const buf = await readAll(s, timeoutMs);
|
|
108
|
+
const b = buf.toString('utf8');
|
|
109
|
+
const m = b.match(/version\s*=?\s*([\d.]+)/i);
|
|
110
|
+
return { type: 'oracle', product: 'Oracle Database', version: m ? m[1] : '', banner: b || 'TNS (no banner)' };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// --- MongoDB (27017) ---
|
|
114
|
+
async function probeMongoDB(host, port, timeoutMs) {
|
|
115
|
+
const s = await connect(host, port, timeoutMs);
|
|
116
|
+
// Build OP_QUERY for { buildInfo: 1 } on admin.$cmd
|
|
117
|
+
const coll = Buffer.from('admin.$cmd\0', 'utf8');
|
|
118
|
+
const doc = buildBsonInt32('buildInfo', 1);
|
|
119
|
+
const headerLen = 16, bodyLen = 4 + coll.length + 4 + 4 + doc.length;
|
|
120
|
+
const buf = Buffer.alloc(headerLen + bodyLen);
|
|
121
|
+
let o = 0;
|
|
122
|
+
buf.writeInt32LE(headerLen + bodyLen, o); o+=4;
|
|
123
|
+
buf.writeInt32LE(1, o); o+=4; // requestID
|
|
124
|
+
buf.writeInt32LE(0, o); o+=4; // responseTo
|
|
125
|
+
buf.writeInt32LE(2004, o); o+=4; // OP_QUERY
|
|
126
|
+
buf.writeInt32LE(0, o); o+=4; // flags
|
|
127
|
+
coll.copy(buf, o); o += coll.length;
|
|
128
|
+
buf.writeInt32LE(0, o); o+=4; // numberToSkip
|
|
129
|
+
buf.writeInt32LE(-1, o); o+=4; // numberToReturn
|
|
130
|
+
doc.copy(buf, o); o += doc.length;
|
|
131
|
+
s.write(buf);
|
|
132
|
+
const rsp = await readAll(s, timeoutMs);
|
|
133
|
+
const txt = rsp.toString('utf8');
|
|
134
|
+
const m = txt.match(/"version"\s*:\s*"([^"]+)"/i);
|
|
135
|
+
const version = m ? m[1] : '';
|
|
136
|
+
return { type: 'mongodb', product: 'MongoDB', version, banner: version ? `MongoDB buildInfo: ${version}` : 'MongoDB (version not disclosed)' };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function buildBsonInt32(key, value) {
|
|
140
|
+
const k = Buffer.from(key + '\0', 'utf8');
|
|
141
|
+
const len = 4 + 1 + k.length + 4 + 1; // size + type + key + int32 + terminator
|
|
142
|
+
const b = Buffer.alloc(len);
|
|
143
|
+
let o = 0;
|
|
144
|
+
b[o++] = 0; // filled after
|
|
145
|
+
b[o++] = 0;
|
|
146
|
+
b[o++] = 0;
|
|
147
|
+
b[o++] = 0;
|
|
148
|
+
b[o++] = 0x10; // int32
|
|
149
|
+
k.copy(b, o); o += k.length;
|
|
150
|
+
b.writeInt32LE(value, o); o+=4;
|
|
151
|
+
b[o++] = 0x00;
|
|
152
|
+
b.writeInt32LE(len, 0);
|
|
153
|
+
return b;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function probeDb(host, port, timeoutMs) {
|
|
157
|
+
switch (Number(port)) {
|
|
158
|
+
case 3306: return await probeMySQL(host, port, timeoutMs);
|
|
159
|
+
case 1433: return await probeMSSQL(host, port, timeoutMs);
|
|
160
|
+
case 5432: return await probePostgres(host, port, timeoutMs);
|
|
161
|
+
case 1521: return await probeOracleTNS(host, port, timeoutMs);
|
|
162
|
+
case 27017: return await probeMongoDB(host, port, timeoutMs);
|
|
163
|
+
default: return null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export default {
|
|
168
|
+
id: '025',
|
|
169
|
+
name: 'DB Scanner',
|
|
170
|
+
description: 'Identifies common databases on well-known ports and extracts product/version banners.',
|
|
171
|
+
priority: 62,
|
|
172
|
+
requirements: { host: 'up', tcp_open: DB_PORTS },
|
|
173
|
+
protocols: ['tcp'],
|
|
174
|
+
ports: DB_PORTS,
|
|
175
|
+
dependencies: [],
|
|
176
|
+
|
|
177
|
+
async run(host, port = 0, opts = {}) {
|
|
178
|
+
const timeoutMs = Number(opts.timeoutMs || process.env.DB_SCAN_TIMEOUT_MS || TIMEOUT);
|
|
179
|
+
const result = {
|
|
180
|
+
up: false,
|
|
181
|
+
program: 'Unknown',
|
|
182
|
+
version: 'Unknown',
|
|
183
|
+
os: null,
|
|
184
|
+
type: 'database',
|
|
185
|
+
data: [],
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
const r = await probeDb(host, Number(port), timeoutMs);
|
|
190
|
+
if (!r) {
|
|
191
|
+
result.data.push({
|
|
192
|
+
probe_protocol: 'tcp',
|
|
193
|
+
probe_port: Number(port),
|
|
194
|
+
probe_info: 'Unsupported DB port',
|
|
195
|
+
response_banner: null
|
|
196
|
+
});
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const program =
|
|
201
|
+
r.product || (r.type ? String(r.type).toUpperCase() : 'Unknown');
|
|
202
|
+
|
|
203
|
+
result.up = true; // a TCP conversation succeeded
|
|
204
|
+
result.program = program;
|
|
205
|
+
result.version = r.version || 'Unknown';
|
|
206
|
+
|
|
207
|
+
result.data.push({
|
|
208
|
+
probe_protocol: 'tcp',
|
|
209
|
+
probe_port: Number(port),
|
|
210
|
+
probe_info: `${program}${r.version ? ' ' + r.version : ''}`.trim(),
|
|
211
|
+
response_banner: r.banner || null
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
return result;
|
|
215
|
+
} catch (e) {
|
|
216
|
+
result.data.push({
|
|
217
|
+
probe_protocol: 'tcp',
|
|
218
|
+
probe_port: Number(port),
|
|
219
|
+
probe_info: `TCP error: ${e?.message || e}`,
|
|
220
|
+
response_banner: null
|
|
221
|
+
});
|
|
222
|
+
return result;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
import { statusFrom } from '../utils/conclusion_utils.mjs';
|
|
228
|
+
|
|
229
|
+
export async function conclude({ host, result }) {
|
|
230
|
+
const rows = Array.isArray(result?.data) ? result.data : [];
|
|
231
|
+
const svcByPort = { 3306:'mysql', 5432:'postgresql', 1433:'mssql', 1521:'oracle', 27017:'mongodb' };
|
|
232
|
+
const items = [];
|
|
233
|
+
for (const r of rows) {
|
|
234
|
+
if (typeof r?.probe_port !== 'number') continue;
|
|
235
|
+
const port = Number(r.probe_port);
|
|
236
|
+
const proto = String(r?.probe_protocol || 'tcp');
|
|
237
|
+
const service = svcByPort[port] || (proto === 'udp' ? `udp-${port}` : `tcp-${port}`);
|
|
238
|
+
const info = r?.probe_info || null;
|
|
239
|
+
const banner = r?.response_banner || null;
|
|
240
|
+
items.push({
|
|
241
|
+
port, protocol: proto, service,
|
|
242
|
+
program: result?.program || 'Unknown', version: result?.version || 'Unknown',
|
|
243
|
+
status: result?.up ? 'open' : statusFrom({ info, banner }),
|
|
244
|
+
info, banner, source: 'db-scanner', evidence: rows, authoritative: true
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
return items;
|
|
248
|
+
}
|