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
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, "&")
|
|
16
|
+
.replace(/</g, "<")
|
|
17
|
+
.replace(/>/g, ">");
|
|
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
|
+
}
|
package/utils/redact.mjs
ADDED
|
@@ -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
|
+
}
|