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.
Files changed (60) hide show
  1. package/CONTRIBUTING.md +24 -0
  2. package/LICENSE +21 -0
  3. package/README.md +584 -0
  4. package/bin/nsauditor-ai-mcp.mjs +2 -0
  5. package/bin/nsauditor-ai.mjs +2 -0
  6. package/cli.mjs +939 -0
  7. package/config/services.json +304 -0
  8. package/docs/EULA-nsauditor-ai.md +324 -0
  9. package/index.mjs +15 -0
  10. package/mcp_server.mjs +382 -0
  11. package/package.json +44 -0
  12. package/plugin_manager.mjs +829 -0
  13. package/plugins/arp_scanner.mjs +162 -0
  14. package/plugins/db_scanner.mjs +248 -0
  15. package/plugins/dns_scanner.mjs +369 -0
  16. package/plugins/dnssd-scanner.mjs +245 -0
  17. package/plugins/ftp_banner_check.mjs +247 -0
  18. package/plugins/host_up_check.mjs +337 -0
  19. package/plugins/http_probe.mjs +290 -0
  20. package/plugins/llmnr_scanner.mjs +130 -0
  21. package/plugins/mdns_scanner.mjs +522 -0
  22. package/plugins/netbios_scanner.mjs +737 -0
  23. package/plugins/opensearch_scanner.mjs +276 -0
  24. package/plugins/os_detector.mjs +436 -0
  25. package/plugins/ping_checker.mjs +271 -0
  26. package/plugins/port_scanner.mjs +250 -0
  27. package/plugins/result_concluder.mjs +274 -0
  28. package/plugins/snmp_scanner.mjs +278 -0
  29. package/plugins/ssh_scanner.mjs +421 -0
  30. package/plugins/sunrpc_scanner.mjs +339 -0
  31. package/plugins/syn_scanner.mjs +314 -0
  32. package/plugins/tls_scanner.mjs +225 -0
  33. package/plugins/upnp_scanner.mjs +441 -0
  34. package/plugins/webapp_detector.mjs +246 -0
  35. package/plugins/wsd_scanner.mjs +290 -0
  36. package/utils/attack_map.mjs +180 -0
  37. package/utils/capabilities.mjs +53 -0
  38. package/utils/conclusion_utils.mjs +70 -0
  39. package/utils/cpe.mjs +74 -0
  40. package/utils/cve_validator.mjs +64 -0
  41. package/utils/cvss.mjs +129 -0
  42. package/utils/delta_reporter.mjs +110 -0
  43. package/utils/export_csv.mjs +82 -0
  44. package/utils/finding_queue.mjs +64 -0
  45. package/utils/finding_schema.mjs +36 -0
  46. package/utils/host_iterator.mjs +166 -0
  47. package/utils/license.mjs +29 -0
  48. package/utils/net_validation.mjs +66 -0
  49. package/utils/nvd_cache.mjs +77 -0
  50. package/utils/nvd_client.mjs +130 -0
  51. package/utils/oui.mjs +107 -0
  52. package/utils/plugin_discovery.mjs +89 -0
  53. package/utils/prompts.mjs +143 -0
  54. package/utils/raw_report_html.mjs +170 -0
  55. package/utils/redact.mjs +79 -0
  56. package/utils/report_html.mjs +236 -0
  57. package/utils/sarif.mjs +225 -0
  58. package/utils/scan_history.mjs +248 -0
  59. package/utils/scheduler.mjs +157 -0
  60. package/utils/webhook.mjs +177 -0
package/utils/oui.mjs ADDED
@@ -0,0 +1,107 @@
1
+ // utils/oui.mjs
2
+
3
+ import fs from "fs/promises";
4
+ import path from "path";
5
+ import { fileURLToPath } from "url";
6
+
7
+ let OUI_DB = null;
8
+
9
+ // Try to import oui-data module, fall back to local file if it fails
10
+ async function loadOuiData() {
11
+ try {
12
+ const ouiData = await import("oui-data", { with: { type: "json" } }).then(module => module.default);
13
+ console.log("[oui.mjs] Successfully loaded oui-data module with", Object.keys(ouiData).length, "entries");
14
+ return ouiData;
15
+ } catch (e) {
16
+ console.error("[oui.mjs] Failed to load oui-data module:", e.message);
17
+ // Fallback to local oui-data.json if available
18
+ try {
19
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
20
+ const localPath = path.join(__dirname, "oui-data.json");
21
+ const data = JSON.parse(await fs.readFile(localPath, "utf8"));
22
+ console.log("[oui.mjs] Loaded fallback oui-data.json from", localPath, "with", Object.keys(data).length, "entries");
23
+ return data;
24
+ } catch (fallbackError) {
25
+ console.error("[oui.mjs] Failed to load fallback oui-data.json:", fallbackError.message);
26
+ return null;
27
+ }
28
+ }
29
+ }
30
+
31
+ // Initialize the database
32
+ export async function initOui() {
33
+ OUI_DB = await loadOuiData();
34
+ return !!OUI_DB;
35
+ }
36
+
37
+ // Convert a MAC address to its Organizationally Unique Identifier (OUI).
38
+ function macToOUI(mac) {
39
+ if (!mac) return null;
40
+ // Strip non-hex characters and take the first 6
41
+ return String(mac).replace(/[^0-9A-Fa-f]/g, '').toUpperCase().slice(0, 6);
42
+ }
43
+
44
+ // Extract the vendor's name from an OUI database entry.
45
+ function pickOrgName(entry) {
46
+ if (!entry) return null;
47
+ if (typeof entry === 'string') return entry;
48
+ return entry.company || entry.organizationName || entry.organization || entry.vendor || entry.name || null;
49
+ }
50
+
51
+ /**
52
+ * Looks up the vendor name for a given MAC address.
53
+ *
54
+ * @param {string} mac The MAC address to look up.
55
+ * @returns {string|null} The vendor name or null if not found.
56
+ */
57
+ export function lookupVendor(mac) {
58
+ // OUI_DB must be initialized via initOui() before calling this function.
59
+ // Callers (plugin_manager.mjs) call initOui() at startup before any plugin runs.
60
+ if (!OUI_DB || !mac || typeof mac !== 'string') {
61
+ return null;
62
+ }
63
+
64
+ try {
65
+ const oui = macToOUI(mac);
66
+ const entry = OUI_DB[oui];
67
+ const vendor = pickOrgName(entry);
68
+ console.log(`[oui.mjs] Lookup for MAC ${mac} (OUI: ${oui}) -> Vendor: ${vendor || 'Not found'}`);
69
+ return vendor;
70
+ } catch (e) {
71
+ console.error("[oui.mjs] Lookup error:", e.message);
72
+ return null;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Heuristically guesses the probable operating system based on the vendor name.
78
+ * This is a highly conservative guess and may be unreliable.
79
+ *
80
+ * @param {string} vendor The vendor name.
81
+ * @returns {string} A short label for the probable OS, or 'Unknown'.
82
+ */
83
+ export function probableOsFromVendor(vendor) {
84
+ const v = (vendor || '').toLowerCase();
85
+ if (!v) return 'Unknown';
86
+
87
+ if (v.includes('apple')) return 'macOS or iOS';
88
+ if (v.includes('samsung')) return 'Android';
89
+ if (v.includes('microsoft')) return 'Windows';
90
+ if (v.includes('google')) return 'Android or ChromeOS';
91
+ if (v.includes('sony')) return 'Android';
92
+ if (v.includes('hewlett') || v.includes('hp ') || v === 'hp') return 'Windows';
93
+ if (v.includes('dell')) return 'Windows';
94
+
95
+ // Common network / IoT vendors -> likely Embedded Linux
96
+ if (v.includes('ring') || v.includes('ubiquiti') || v.includes('tp-link') ||
97
+ v.includes('tplink') || v.includes('netgear') || v.includes('mikrotik') ||
98
+ v.includes('synology') || v.includes('qnap') || v.includes('hikvision') ||
99
+ v.includes('dahua') || v.includes('arris') || v.includes('avm') ||
100
+ v.includes('asus') || v.includes('open-mesh') || v.includes('linksys') ||
101
+ v.includes('tenda') || v.includes('roku') || v.includes('philips') ||
102
+ v.includes('lg') || v.includes('xiaomi') || v.includes('huawei')) {
103
+ return 'Embedded Linux';
104
+ }
105
+
106
+ return 'Unknown';
107
+ }
@@ -0,0 +1,89 @@
1
+ // utils/plugin_discovery.mjs
2
+ // Multi-path plugin loader: CE built-in, EE package (optional), custom (env var)
3
+
4
+ import { readdir } from 'node:fs/promises';
5
+ import { existsSync, realpathSync } from 'node:fs';
6
+ import { join, resolve, dirname } from 'node:path';
7
+ import { createRequire } from 'node:module';
8
+
9
+ const _require = createRequire(import.meta.url);
10
+
11
+ const SAFE_PREFIXES = Object.freeze(
12
+ [process.cwd(), process.env.HOME].filter(Boolean).map(p => p.endsWith('/') ? p : `${p}/`)
13
+ );
14
+
15
+ function isSafePath(absPath) {
16
+ // Allow exact match (e.g. NSAUDITOR_PLUGIN_PATH=./plugins resolves to cwd itself)
17
+ // or any subtree under cwd or HOME.
18
+ // Symlinks are dereferenced via realpathSync before this check is called.
19
+ return SAFE_PREFIXES.some(prefix => absPath === prefix.slice(0, -1) || absPath.startsWith(prefix));
20
+ }
21
+
22
+ async function loadPluginsFromDir(dir, source) {
23
+ let files;
24
+ try {
25
+ files = await readdir(dir);
26
+ } catch {
27
+ return [];
28
+ }
29
+
30
+ const plugins = [];
31
+ for (const file of files.filter(f => f.endsWith('.mjs'))) {
32
+ try {
33
+ const mod = await import(join(dir, file));
34
+ const plugin = mod.default;
35
+ if (plugin?.id && plugin?.name && typeof plugin?.run === 'function') {
36
+ // Attach conclude from named export or plugin default
37
+ const conclude = mod.conclude ?? plugin.conclude;
38
+ plugins.push({ ...plugin, _source: source, ...(conclude ? { conclude } : {}) });
39
+ }
40
+ } catch (e) {
41
+ if (process.env.NSA_VERBOSE) {
42
+ console.error(`[plugin_discovery] Failed to load ${file}: ${e.message}`);
43
+ }
44
+ }
45
+ }
46
+ return plugins;
47
+ }
48
+
49
+ export async function discoverPlugins(baseDir) {
50
+ const plugins = [];
51
+
52
+ // Source 1: CE built-in plugins
53
+ plugins.push(...await loadPluginsFromDir(join(baseDir, 'plugins'), 'ce'));
54
+
55
+ // Source 2: EE package (@nsasoft/nsauditor-ai-ee)
56
+ try {
57
+ const eePkgPath = _require.resolve('@nsasoft/nsauditor-ai-ee/package.json');
58
+ const eePluginsDir = join(dirname(eePkgPath), 'plugins');
59
+ if (existsSync(eePluginsDir)) {
60
+ plugins.push(...await loadPluginsFromDir(eePluginsDir, 'ee'));
61
+ }
62
+ } catch {
63
+ // EE not installed — CE operates standalone
64
+ }
65
+
66
+ // Source 3: Custom plugin paths (colon-separated)
67
+ const customPaths = process.env.NSAUDITOR_PLUGIN_PATH;
68
+
69
+ if (customPaths) {
70
+ for (const dir of customPaths.split(':')) {
71
+ const abs = resolve(dir);
72
+ let real;
73
+ try {
74
+ real = realpathSync(abs);
75
+ } catch {
76
+ // Path doesn't exist or is inaccessible — skip silently unless verbose
77
+ if (process.env.NSA_VERBOSE) console.warn(`[plugin_discovery] Cannot resolve real path for: ${abs}`);
78
+ continue;
79
+ }
80
+ if (!isSafePath(real)) {
81
+ if (process.env.NSA_VERBOSE) console.warn(`[plugin_discovery] Skipping unsafe NSAUDITOR_PLUGIN_PATH entry: ${real}`);
82
+ continue;
83
+ }
84
+ plugins.push(...await loadPluginsFromDir(real, 'custom'));
85
+ }
86
+ }
87
+
88
+ return plugins.sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
89
+ }
@@ -0,0 +1,143 @@
1
+ export const openaiSimplePrompt = 'You are a network security assistant. Summarize findings and suggest next actions.';
2
+
3
+ export const openaiPrompt = `You are a senior network security analyst with expertise in assessing vulnerabilities in networks, applications, and systems. Your goals are to detect cybersecurity threats, evaluate risks, and recommend mitigations to safeguard organizational networks and data.
4
+
5
+ You are given a JSON payload produced by a network audit tool. It contains a high-level summary, a normalized services list, and raw probe evidence lines. Use ONLY this payload; do not assume anything not present in it.
6
+
7
+ Rules you must follow:
8
+ - Be factual and avoid speculation. If a product AND version are not both present in the payload evidence (e.g., service banner, header, or explicit "program"+"version"), do NOT map to a CVE. Place such items in the "Leads (Needs Verification)" table instead.
9
+ - When you do map to a CVE, explicitly quote the exact evidence line that proves the product+version (for example, the FTP banner, HTTP Server header, DNS version.bind, etc.).
10
+ - If the payload contains a placeholder like "[REDACTED_HIDDEN]" (e.g., Serial=[REDACTED_HIDDEN]), keep it redacted and never invent values. Do NOT add a secret if none is present.
11
+ - Treat technologies detected by "Webapp Detector" as leads unless a specific version is present in the payload.
12
+ - If OS is unknown or only weakly hinted, do not guess; state it as unknown or inferred with low confidence.
13
+ - At the start of the Executive Summary, include a single line: OS (from scan): <value>. Parse <value> from the payload summary text if it contains "Likely OS: ...". If not present, write "Unknown".
14
+
15
+ Structure your response in markdown with the following sections:
16
+
17
+ ## Executive Summary
18
+ - First line: **OS (from scan): <value>** (derived strictly from the payload summary "Likely OS: ..." text if present; otherwise "Unknown").
19
+ - Provide a high-level overview for leadership: overall security posture, notable internet-exposed services, and the most critical, confirmed vulnerabilities (if any).
20
+
21
+ ## Detailed Vulnerability Analysis
22
+ Provide two tables:
23
+
24
+ 1) Confirmed Vulnerabilities (only when product+version are proven by evidence)
25
+ Columns: Vulnerability | Affected Asset/Port | Severity (Critical/High/Medium/Low) | CVSS v3.1 (if known) | CVE ID | Evidence (quote the exact line)
26
+ - Only include rows where the product and version are explicitly present in the payload.
27
+ - If CVSS is unknown, use "—".
28
+ - The Evidence cell must include the exact banner/header line that proves the mapping.
29
+
30
+ 2) Leads (Needs Verification)
31
+ Columns: Technology or Product | Where Detected (service/port or detector) | What to Verify | Why it Matters
32
+ - Include technologies or products inferred from headers, banners, or the Webapp Detector where version is missing.
33
+ - Do NOT assign CVEs in this table; list the verification step needed to confirm a vulnerable version.
34
+
35
+ ## Risk Assessment
36
+ - Explain potential business impact if issues remain unaddressed. Tie each risk to an item from the Confirmed table where possible. If there are no confirmed vulnerabilities, focus on exposure risks and misconfigurations indicated by open services.
37
+
38
+ ## Prioritized Remediation Plan
39
+ - Provide a numbered list of actionable steps, prioritized by severity and business impact.
40
+ - For each step: the action (what), rationale (why it matters), and priority (Critical/High/Medium/Low). Include suggested timelines (e.g., immediate, within 7 days, within 30 days).
41
+ - Where items appear in the Leads table, include lightweight verification steps (e.g., retrieve precise version, run safe banner grabs, check admin UI build/version).
42
+
43
+ ## Next Steps and Continuous Monitoring
44
+ - Suggest follow-up actions: re-scan after changes, enable logging/alerts, inventory of exposed services, version tracking, periodic WAF/IDS/IPS checks, and scheduled audits.
45
+
46
+ Output must be concise, professional, and defensible. Avoid speculative language; prefer “confirmed” vs “needs verification”.`;
47
+
48
+ export const openaiPromptOptimized = `You are a senior network security analyst specializing in vulnerability assessment across networks, applications, and systems. Your mission is to detect cybersecurity threats, evaluate risks, and recommend actionable mitigation strategies.
49
+
50
+ ## Analysis Approach
51
+ Before starting, outline your analysis strategy in 3-5 bullets covering:
52
+ - Payload validation and structure verification
53
+ - Evidence categorization methodology
54
+ - Vulnerability confirmation criteria
55
+ - Risk prioritization framework
56
+
57
+ ## Input Requirements
58
+ You will receive a JSON payload from a network audit tool containing:
59
+ - **summary**: High-level scan overview with potential OS detection
60
+ - **services**: Normalized service list with ports, protocols, and banners
61
+ - **evidence**: Raw probe data and detection results
62
+
63
+ **Critical Rules:**
64
+ 1. **Strict Evidence-Based Analysis**: Only map to CVEs when BOTH product AND version are explicitly present in the payload evidence
65
+ 2. **No Speculation**: Use only information contained in the payload - never assume or invent data
66
+ 3. **Redaction Respect**: Preserve any placeholders like "[REDACTED_HIDDEN]" without substitution
67
+ 4. **Webapp Detector Handling**: Treat as confirmed only if version is present; otherwise categorize as leads
68
+ 5. **OS Detection**: Extract from summary "Likely OS: ..." pattern only; default to "Unknown" if absent
69
+
70
+ ## Required Output Structure
71
+
72
+ ### Executive Summary
73
+ **First Line**: **OS (from scan): [value]** (extract from summary "Likely OS: ..." or state "Unknown")
74
+
75
+ Provide executive-level overview covering:
76
+ - Overall security posture assessment
77
+ - Critical internet-exposed services
78
+ - Confirmed high-severity vulnerabilities
79
+ - Business risk summary
80
+
81
+ ### Detailed Vulnerability Analysis
82
+
83
+ **Table 1: Confirmed Vulnerabilities** (sort by Severity DESC, then Port ASC)
84
+ | Vulnerability | Affected Asset/Port | Severity | CVSS v3.1 | CVE ID | Evidence |
85
+ |---------------|-------------------|----------|-----------|---------|----------|
86
+
87
+ *Requirements*:
88
+ - Include only when product+version are both explicitly present
89
+ - Quote exact evidence line that confirms the mapping
90
+ - Use "—" for unknown CVSS scores
91
+ - Severity: Critical/High/Medium/Low
92
+
93
+ **Table 2: Leads (Needs Verification)** (sort alphabetically by Technology)
94
+ | Technology/Product | Detection Source | Verification Needed | Risk Context |
95
+ |-------------------|------------------|-------------------|--------------|
96
+
97
+ *Requirements*:
98
+ - Include products/technologies without confirmed versions
99
+ - Specify detection method (banner, header, webapp detector, etc.)
100
+ - Define specific verification steps needed
101
+ - No CVE assignments in this table
102
+
103
+ ### Risk Assessment
104
+ Analyze business impact potential for each confirmed vulnerability, linking to:
105
+ - Data exposure risks
106
+ - System compromise scenarios
107
+ - Service availability threats
108
+ - Compliance implications
109
+
110
+ ### Prioritized Remediation Plan
111
+ Numbered action items prioritized by severity and business impact:
112
+
113
+ **Format**:
114
+ 1. **Action**: [What to do]
115
+ - **Rationale**: [Why critical]
116
+ - **Priority**: [Critical/High/Medium/Low]
117
+ - **Timeline**: [Immediate/7 days/30 days]
118
+ - **Verification**: [How to confirm completion]
119
+
120
+ ### Next Steps and Continuous Monitoring
121
+ Recommend ongoing security practices:
122
+ - Post-remediation validation scanning
123
+ - Continuous vulnerability monitoring
124
+ - Asset inventory maintenance
125
+ - Security control implementation
126
+ - Scheduled assessment cadence
127
+
128
+ ## Quality Assurance
129
+ Before finalizing, verify:
130
+ - [ ] All evidence claims are directly quotable from payload
131
+ - [ ] No speculative or assumed information included
132
+ - [ ] Required sections present and properly formatted
133
+ - [ ] CVE mappings have supporting product+version evidence
134
+ - [ ] Tables follow specified sort order
135
+
136
+ ## Error Handling
137
+ If payload is malformed or missing required sections (summary, services, evidence), output:
138
+
139
+ ERROR: Malformed payload detected
140
+ Missing sections: [list missing sections]
141
+ Cannot proceed with analysis - payload validation failed
142
+
143
+ Produce a professional, defensible assessment using precise language. Distinguish between "confirmed vulnerabilities" and "leads requiring verification" throughout your analysis.`;
@@ -0,0 +1,170 @@
1
+ // utils/raw_report_html.mjs
2
+ // Admin RAW HTML report: show open services + full evidence (with status)
3
+ // Emits data-key="tcp-<port>" on open-service rows for tests (e.g., "tcp-80")
4
+
5
+ export async function buildAdminRawHtmlReport({
6
+ host,
7
+ whenIso,
8
+ summary,
9
+ os = null,
10
+ services = [],
11
+ evidence = [],
12
+ } = {}) {
13
+ const esc = (s) =>
14
+ String(s ?? "")
15
+ .replace(/&/g, "&amp;")
16
+ .replace(/</g, "&lt;")
17
+ .replace(/>/g, "&gt;");
18
+
19
+ const guessStatus = (text) => {
20
+ const t = String(text || "");
21
+ if (/success|open\b/i.test(t)) return "open";
22
+ if (/refused|closed|reset|rst/i.test(t)) return "closed";
23
+ if (/timeout|no\s*response|unreachable|filtered/i.test(t)) return "filtered";
24
+ return "";
25
+ };
26
+
27
+ // Build a quick lookup to enrich Evidence “Status” from services
28
+ const svcByKey = new Map();
29
+ for (const s of Array.isArray(services) ? services : []) {
30
+ const key = `${String(s.protocol || "").toLowerCase()}/${Number(s.port)}`;
31
+ if (!svcByKey.has(key)) svcByKey.set(key, s);
32
+ }
33
+
34
+ const openServices = services.filter((s) => String(s.status).toLowerCase() === "open");
35
+
36
+ const openRows = openServices
37
+ .map((s) => {
38
+ const proto = String(s.protocol || "").toLowerCase();
39
+ const key = `${proto}-${Number(s.port)}`; // <-- this puts tcp-80 in the HTML
40
+ return `<tr data-key="${esc(key)}">
41
+ <td>${Number(s.port) || ""}</td>
42
+ <td>${esc(proto)}</td>
43
+ <td>${esc(s.service || "")}</td>
44
+ <td>${esc(s.program || "")}</td>
45
+ <td>${esc(s.version || "")}</td>
46
+ <td>${esc(s.status || "")}</td>
47
+ <td>${esc(s.info || "")}</td>
48
+ <td>${s.banner ? `<pre>${esc(s.banner)}</pre>` : ""}</td>
49
+ </tr>`;
50
+ })
51
+ .join("");
52
+
53
+ const evRows = (Array.isArray(evidence) ? evidence : [])
54
+ .map((e) => {
55
+ // normalize evidence (supports both normalized and legacy probe_* fields)
56
+ const proto = String(e?.protocol ?? e?.probe_protocol ?? "").toLowerCase();
57
+ const port = Number(e?.port ?? e?.probe_port) || "";
58
+ const info = e?.info ?? e?.probe_info ?? "";
59
+ const banner = e?.banner ?? e?.response_banner ?? "";
60
+ const key = `${proto}/${port}`;
61
+ const svc = svcByKey.get(key);
62
+ // Prefer service.status, then explicit evidence status, then inference
63
+ const status = svc?.status || e?.status || guessStatus(info) || guessStatus(banner) || "";
64
+ return `<tr data-proto="${esc(proto)}" data-port="${port}" data-status="${esc(
65
+ String(status).toLowerCase()
66
+ )}">
67
+ <td>${esc(e?.from || "")}</td>
68
+ <td>${esc(proto)}</td>
69
+ <td>${port}</td>
70
+ <td class="ev-status">${esc(status)}</td>
71
+ <td>${esc(info)}</td>
72
+ <td>${banner ? `<pre>${esc(banner)}</pre>` : ""}</td>
73
+ </tr>`;
74
+ })
75
+ .join("");
76
+
77
+ return `<!doctype html>
78
+ <html lang="en">
79
+ <meta charset="utf-8">
80
+ <meta name="viewport" content="width=device-width,initial-scale=1"/>
81
+ <title>Admin Raw Report – ${esc(host)}</title>
82
+ <style>
83
+ :root{
84
+ --bg:#0b0f14; --card:#0f1622; --text:#e7eef7; --muted:#a9b6c6;
85
+ --border:#1f2937; --link:#60a5fa;
86
+ --open:#15a34a; --closed:#ef4444; --filtered:#f59e0b;
87
+ }
88
+ *{box-sizing:border-box}
89
+ body{margin:0;background:var(--bg);color:var(--text);font:14px/1.55 system-ui,Segoe UI,Roboto,Ubuntu,sans-serif}
90
+ .main{max-width:1100px;margin:24px auto;padding:24px;background:var(--card);border:1px solid var(--border);border-radius:14px}
91
+ h1,h2{margin:1.2em 0 .6em}
92
+ h1{font-size:26px} h2{font-size:18px}
93
+ .meta{color:var(--muted);font-size:12px;margin-top:2px}
94
+ .header{border-bottom:1px dashed var(--border);padding-bottom:12px;margin-bottom:18px}
95
+ table{width:100%;border-collapse:collapse;margin:14px 0;border:1px solid var(--border)}
96
+ th,td{border:1px solid var(--border);padding:8px 10px;vertical-align:top}
97
+ th{background:#0e1420;color:#cfe0ff;text-align:left}
98
+ tbody tr:nth-child(odd){background:#0f1522}
99
+ pre{margin:0;white-space:pre-wrap;word-break:break-word}
100
+ .badge{display:inline-block;padding:2px 8px;border-radius:999px;font-weight:600;border:1px solid transparent}
101
+ .badge-open{background:rgba(21,163,74,.08);border-color:var(--open);color:#b9f6ca}
102
+ .badge-closed{background:rgba(239,68,68,.08);border-color:var(--closed);color:#fecaca}
103
+ .badge-filtered{background:rgba(245,158,11,.08);border-color:var(--filtered);color:#ffe4b5}
104
+ .controls{display:flex;gap:16px;align-items:center;margin:8px 0 14px;color:var(--muted)}
105
+ </style>
106
+ <body>
107
+ <div class="main">
108
+ <div class="header">
109
+ <h1>Admin Raw Report – ${esc(host)}</h1>
110
+ <div class="meta">Generated: ${esc(whenIso || new Date().toISOString())}${os ? ` • OS: ${esc(os)}` : ""}</div>
111
+ ${summary ? `<div class="meta">Summary: ${esc(summary)}</div>` : ""}
112
+ </div>
113
+
114
+ <h2>Open Services</h2>
115
+ <table>
116
+ <thead>
117
+ <tr>
118
+ <th>Port</th><th>Protocol</th><th>Service</th><th>Program</th><th>Version</th><th>Status</th><th>Info</th><th>Banner</th>
119
+ </tr>
120
+ </thead>
121
+ <tbody>
122
+ ${openRows || ""}
123
+ </tbody>
124
+ </table>
125
+
126
+ <div class="controls">
127
+ <label><input type="checkbox" id="onlyOpen"> Show only OPEN in Evidence</label>
128
+ </div>
129
+
130
+ <h2>Evidence</h2>
131
+ <table id="ev">
132
+ <thead>
133
+ <tr>
134
+ <th>From</th><th>Protocol</th><th>Port</th><th>Status</th><th>Info</th><th>Banner</th>
135
+ </tr>
136
+ </thead>
137
+ <tbody>
138
+ ${evRows || ""}
139
+ </tbody>
140
+ </table>
141
+ </div>
142
+
143
+ <script>
144
+ (function(){
145
+ const onlyOpen = document.getElementById('onlyOpen');
146
+ const tbl = document.getElementById('ev');
147
+ if (!onlyOpen || !tbl) return;
148
+ function apply(){
149
+ const want = !!onlyOpen.checked;
150
+ tbl.querySelectorAll('tbody tr').forEach(tr=>{
151
+ const st = (tr.getAttribute('data-status')||'').toLowerCase();
152
+ tr.style.display = (want && st !== 'open') ? 'none' : '';
153
+ // badge styling in-place:
154
+ const cell = tr.querySelector('.ev-status');
155
+ if (!cell) return;
156
+ const v = (cell.textContent||'').trim().toLowerCase();
157
+ let cls = '';
158
+ if (v === 'open') cls = 'badge badge-open';
159
+ else if (v === 'closed') cls = 'badge badge-closed';
160
+ else if (v === 'filtered') cls = 'badge badge-filtered';
161
+ if (cls) cell.innerHTML = '<span class="'+cls+'">'+cell.textContent.trim()+'</span>';
162
+ });
163
+ }
164
+ onlyOpen.addEventListener('change', apply);
165
+ apply();
166
+ })();
167
+ </script>
168
+ </body>
169
+ </html>`;
170
+ }
@@ -0,0 +1,79 @@
1
+ // utils/redact.mjs
2
+ // Replace values whose KEY contains any of the given keywords.
3
+ // Example: scrubByKey({ serialNumber: 'X8K...' }, ['serial']) -> { serialNumber: '[REDACTED_HIDDEN]' }
4
+
5
+ export function scrubByKey(val, keywords, placeholder = '[REDACTED_HIDDEN]') {
6
+ if (val == null) return val;
7
+
8
+ if (Array.isArray(val)) {
9
+ return val.map(v => scrubByKey(v, keywords, placeholder));
10
+ }
11
+
12
+ if (typeof val === 'object') {
13
+ const out = {};
14
+ for (const [k, v] of Object.entries(val)) {
15
+ const lk = k.toLowerCase();
16
+ const hit = keywords.some(word => lk.includes(word));
17
+ out[k] = hit ? placeholder : scrubByKey(v, keywords, placeholder);
18
+ }
19
+ return out;
20
+ }
21
+
22
+ return val; // primitives pass through
23
+ }
24
+
25
+ // Pattern-based redaction rules
26
+ const PATTERNS = [
27
+ // Standard level
28
+ { regex: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g, replacement: '[REDACTED_EMAIL]', level: 'standard' },
29
+ { regex: /\b[\w-]+\.(internal|local|corp|lan|intra|priv)\b/gi, replacement: '[REDACTED_HOSTNAME]', level: 'standard' },
30
+ { regex: /https?:\/\/(?:10\.|172\.(?:1[6-9]|2\d|3[01])\.|192\.168\.)\S+/g, replacement: '[REDACTED_URL]', level: 'standard' },
31
+ { regex: /community=\S+/gi, replacement: 'community=[REDACTED]', level: 'standard' },
32
+ { regex: /\b(?:[0-9a-f]{2}:){5}[0-9a-f]{2}\b/gi, replacement: '[REDACTED_MAC]', level: 'standard' },
33
+ // Strict level
34
+ { regex: /(?:\/[\w.-]+){2,}\.(?:conf|log|ini|cfg|env|key|pem|crt)\b/g, replacement: '[REDACTED_PATH]', level: 'strict' },
35
+ { regex: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g, replacement: '[REDACTED_AWS_KEY]', level: 'strict' },
36
+ { regex: /\bBearer\s+[A-Za-z0-9._~+\/=-]+/gi, replacement: '[REDACTED_BEARER]', level: 'strict' },
37
+ ];
38
+
39
+ /**
40
+ * Redact sensitive patterns from string values within an object tree.
41
+ * Applies regex-based redaction to all string values recursively.
42
+ * @param {*} val - Value to redact
43
+ * @param {string} level - 'standard' or 'strict'
44
+ * @returns {*} Redacted value
45
+ */
46
+ export function redactPatterns(val, level = 'standard') {
47
+ const activePatterns = PATTERNS.filter(
48
+ p => p.level === 'standard' || (level === 'strict' && p.level === 'strict')
49
+ );
50
+ return _applyPatterns(val, activePatterns);
51
+ }
52
+
53
+ function _applyPatterns(val, patterns) {
54
+ if (val == null) return val;
55
+
56
+ if (typeof val === 'string') {
57
+ let result = val;
58
+ for (const { regex, replacement } of patterns) {
59
+ // Reset lastIndex for global regexes
60
+ regex.lastIndex = 0;
61
+ result = result.replace(regex, replacement);
62
+ }
63
+ return result;
64
+ }
65
+
66
+ if (Array.isArray(val)) {
67
+ return val.map(v => _applyPatterns(v, patterns));
68
+ }
69
+
70
+ if (typeof val === 'object') {
71
+ const out = {};
72
+ for (const [k, v] of Object.entries(val)) {
73
+ out[k] = _applyPatterns(v, patterns);
74
+ }
75
+ return out;
76
+ }
77
+
78
+ return val;
79
+ }