Download lib/recon-analysis.mjs from teddyoas/Asnip: direct link, hf CLI and curl.
- Browser
- Download file 10.5 kB
-
https://huggingface.co/teddyoas/Asnip/resolve/main/lib/recon-analysis.mjs
- Command line
-
hf download hf://teddyoas/Asnip/lib/recon-analysis.mjs
-
curl -L -o recon-analysis.mjs https://huggingface.co/teddyoas/Asnip/resolve/main/lib/recon-analysis.mjs
10.5 kB
| import fs from "fs"; | |
| import { resolve } from "path"; | |
| import { homedir } from "os"; | |
| const MISSIONS_DIR = resolve(homedir(), ".config", "phantom", "missions"); | |
| function parseSubfinderOutput(output, target, phase) { | |
| const results = []; | |
| if (!output || typeof output !== "string") return results; | |
| for (const line of output.split("\n")) { | |
| const trimmed = line.trim(); | |
| if (!trimmed || trimmed.startsWith("[") || trimmed.startsWith("Error")) continue; | |
| const subdomain = trimmed.toLowerCase().replace(/^https?:\/\//, "").split("/")[0]; | |
| if (subdomain && subdomain.includes(".")) { | |
| results.push({ | |
| type: "subdomain", | |
| value: subdomain, | |
| target, | |
| phase, | |
| source: "subfinder", | |
| timestamp: new Date().toISOString(), | |
| status: "success" | |
| }); | |
| } | |
| } | |
| return results; | |
| } | |
| function parseDnsxOutput(output, target, phase) { | |
| const results = []; | |
| if (!output || typeof output !== "string") return results; | |
| for (const line of output.split("\n")) { | |
| const trimmed = line.trim(); | |
| if (!trimmed) continue; | |
| try { | |
| const parsed = JSON.parse(trimmed); | |
| if (parsed.host) { | |
| results.push({ | |
| type: "dns_record", | |
| name: parsed.host, | |
| recordType: parsed.a ? "A" : parsed.aaaa ? "AAAA" : parsed.cname ? "CNAME" : parsed.txt ? "TXT" : parsed.mx ? "MX" : parsed.ns ? "NS" : "UNKNOWN", | |
| value: parsed.a || parsed.aaaa || parsed.cname || parsed.txt || parsed.mx || parsed.ns || "", | |
| target, | |
| phase, | |
| source: "dnsx", | |
| timestamp: new Date().toISOString(), | |
| status: "success" | |
| }); | |
| } | |
| } catch { | |
| const parts = trimmed.split(/\s+/); | |
| if (parts.length >= 3) { | |
| results.push({ | |
| type: "dns_record", | |
| name: parts[0], | |
| recordType: parts[1].toUpperCase(), | |
| value: parts.slice(2).join(" "), | |
| target, | |
| phase, | |
| source: "dnsx", | |
| timestamp: new Date().toISOString(), | |
| status: "success" | |
| }); | |
| } | |
| } | |
| } | |
| return results; | |
| } | |
| function parseHttpxOutput(output, target, phase) { | |
| const results = []; | |
| if (!output || typeof output !== "string") return results; | |
| for (const line of output.split("\n")) { | |
| const trimmed = line.trim(); | |
| if (!trimmed) continue; | |
| try { | |
| const parsed = JSON.parse(trimmed); | |
| if (parsed.url) { | |
| results.push({ | |
| type: "http_service", | |
| url: parsed.url, | |
| statusCode: parsed.status_code, | |
| contentLength: parsed.content_length, | |
| technology: parsed.tech ? parsed.tech.map(t => t.toLowerCase()) : [], | |
| title: parsed.title || "", | |
| webServer: parsed.webserver || "", | |
| target, | |
| phase, | |
| source: "httpx", | |
| timestamp: new Date().toISOString(), | |
| status: "success" | |
| }); | |
| } | |
| } catch { | |
| const urlMatch = trimmed.match(/https?:\/\/[^\s]+/); | |
| if (urlMatch) { | |
| results.push({ | |
| type: "http_service", | |
| url: urlMatch[0], | |
| target, | |
| phase, | |
| source: "httpx", | |
| timestamp: new Date().toISOString(), | |
| status: "success" | |
| }); | |
| } | |
| } | |
| } | |
| return results; | |
| } | |
| function parseNucleiOutput(output, target, phase) { | |
| const results = []; | |
| if (!output || typeof output !== "string") return results; | |
| for (const line of output.split("\n")) { | |
| const trimmed = line.trim(); | |
| if (!trimmed) continue; | |
| try { | |
| const parsed = JSON.parse(trimmed); | |
| if (parsed.template_id) { | |
| results.push({ | |
| type: "security_metadata", | |
| templateId: parsed.template_id, | |
| severity: parsed.info?.severity || "info", | |
| name: parsed.info?.name || "", | |
| description: parsed.info?.description || "", | |
| matchedAt: parsed.matched_at, | |
| target, | |
| phase, | |
| source: "nuclei", | |
| timestamp: new Date().toISOString(), | |
| status: "success" | |
| }); | |
| } | |
| } catch { | |
| const sevMatch = trimmed.match(/\[(critical|high|medium|low|info)\]/i); | |
| if (sevMatch) { | |
| results.push({ | |
| type: "security_metadata", | |
| raw: trimmed, | |
| severity: sevMatch[1].toLowerCase(), | |
| target, | |
| phase, | |
| source: "nuclei", | |
| timestamp: new Date().toISOString(), | |
| status: "success" | |
| }); | |
| } | |
| } | |
| } | |
| return results; | |
| } | |
| function parseTLSOutput(output, target, phase) { | |
| const results = []; | |
| if (!output || typeof output !== "string") return results; | |
| try { | |
| const parsed = JSON.parse(output); | |
| if (parsed.subject || parsed.issuer || parsed.notAfter || parsed.notBefore || parsed.sans) { | |
| results.push({ | |
| type: "tls_certificate", | |
| subject: parsed.subject || "", | |
| issuer: parsed.issuer || "", | |
| notBefore: parsed.notBefore || "", | |
| notAfter: parsed.notAfter || "", | |
| sans: parsed.sans || [], | |
| fingerprint: parsed.fingerprint || "", | |
| target, | |
| phase, | |
| source: "tlsx", | |
| timestamp: new Date().toISOString(), | |
| status: "success" | |
| }); | |
| } | |
| } catch { | |
| // Text format - minimal parsing | |
| const certMatch = output.match(/subject=(.+)/i) || output.match(/issuer=(.+)/i); | |
| if (certMatch) { | |
| results.push({ | |
| type: "tls_certificate", | |
| raw: output, | |
| target, | |
| phase, | |
| source: "tlsx", | |
| timestamp: new Date().toISOString(), | |
| status: "success" | |
| }); | |
| } | |
| } | |
| return results; | |
| } | |
| function parseTechnologyOutput(output, target, phase, source) { | |
| const results = []; | |
| if (!output || typeof output !== "string") return results; | |
| const techPatterns = [ | |
| /nginx\/[\d.]+/gi, /apache\/[\d.]+/gi, /iis\/[\d.]+/gi, /openresty\/[\d.]+/gi, | |
| /cloudflare/gi, /aws/gi, /google/gi, /microsoft/gi, /cloudfront/gi, /akamai/gi, | |
| /wordpress/gi, /drupal/gi, /joomla/gi, /magento/gi, | |
| /php\/[\d.]+/gi, /python\/[\d.]+/gi, /node\.js/gi, /express/gi, /django/gi, /rails/gi, /laravel/gi, | |
| /react/gi, /vue/gi, /angular/gi, /next\.js/gi, /nuxt/gi, | |
| /kubernetes/gi, /docker/gi, /traefik/gi, /envoy/gi, /haproxy/gi, | |
| /postgresql/gi, /mysql/gi, /mongodb/gi, /redis/gi, | |
| /grafana/gi, /prometheus/gi, /kibana/gi, | |
| /jenkins/gi, /gitlab/gi, /github/gi, /bitbucket/gi, | |
| /ssh/gi, /ftp/gi, /smtp/gi, /imap/gi | |
| ]; | |
| for (const pattern of techPatterns) { | |
| const matches = output.match(pattern); | |
| if (matches) { | |
| for (const match of matches) { | |
| results.push({ | |
| type: "technology", | |
| name: match.toLowerCase(), | |
| source, | |
| target, | |
| phase, | |
| timestamp: new Date().toISOString(), | |
| status: "success" | |
| }); | |
| } | |
| } | |
| } | |
| return results; | |
| } | |
| export function normalizeObservations(missionId) { | |
| const missionDir = resolve(MISSIONS_DIR, missionId); | |
| if (!fs.existsSync(missionDir)) { | |
| return { observations: [], errors: ["Mission directory not found"] }; | |
| } | |
| const observations = []; | |
| const errors = []; | |
| const phaseDirs = ["phase1_passive", "phase2_active", "phase3_validation"]; | |
| for (const phaseDir of phaseDirs) { | |
| const dir = resolve(missionDir, phaseDir); | |
| if (!fs.existsSync(dir)) continue; | |
| const files = fs.readdirSync(dir).filter(f => f.endsWith(".json")); | |
| for (const file of files) { | |
| try { | |
| const content = JSON.parse(fs.readFileSync(resolve(dir, file), "utf-8")); | |
| const result = content.result; | |
| const tool = content.tool; | |
| const target = content.target; | |
| const phase = content.phase; | |
| const savedAt = content.savedAt; | |
| if (!result) continue; | |
| let parsed = []; | |
| switch (tool) { | |
| case "subfinder": | |
| parsed = parseSubfinderOutput(result, target, phase); | |
| break; | |
| case "dnsx": | |
| parsed = parseDnsxOutput(result, target, phase); | |
| break; | |
| case "httpx": | |
| parsed = parseHttpxOutput(result, target, phase); | |
| break; | |
| case "nuclei": | |
| parsed = parseNucleiOutput(result, target, phase); | |
| break; | |
| case "tlsx": | |
| parsed = parseTLSOutput(result, target, phase); | |
| break; | |
| case "whatweb": | |
| case "wappalyzer": | |
| case "techdetect": | |
| parsed = parseTechnologyOutput(result, target, phase, tool); | |
| break; | |
| } | |
| // Extract technologies from any tool output | |
| if (typeof result === "string") { | |
| parsed.push(...parseTechnologyOutput(result, target, phase, tool)); | |
| } | |
| // Add savedAt timestamp if available | |
| for (const obs of parsed) { | |
| if (savedAt) obs.savedAt = savedAt; | |
| } | |
| observations.push(...parsed); | |
| } catch (e) { | |
| errors.push({ file, error: e.message }); | |
| } | |
| } | |
| } | |
| // Also parse error files to capture failed tool status | |
| const errorsDir = resolve(missionDir, "errors"); | |
| if (fs.existsSync(errorsDir)) { | |
| const errorFiles = fs.readdirSync(errorsDir); | |
| for (const file of errorFiles) { | |
| try { | |
| const content = JSON.parse(fs.readFileSync(resolve(errorsDir, file), "utf-8")); | |
| observations.push({ | |
| type: "error", | |
| tool: content.tool, | |
| target: content.target, | |
| phase: content.phase, | |
| error: content.error, | |
| timestamp: content.timestamp, | |
| status: "error" | |
| }); | |
| } catch { | |
| // Ignore parse errors in error files | |
| } | |
| } | |
| } | |
| return { observations, errors }; | |
| } | |
| export function loadMissionResults(missionId) { | |
| const { observations, errors } = normalizeObservations(missionId); | |
| return observations; | |
| } | |
| export function loadMissionErrors(missionId) { | |
| const { observations } = normalizeObservations(missionId); | |
| return observations.filter(o => o.status === "error"); | |
| } | |
| export function getObservationsByType(missionId, type) { | |
| const { observations } = normalizeObservations(missionId); | |
| return observations.filter(o => o.type === type && o.status === "success"); | |
| } | |
| export function getObservationsByPhase(missionId, phase) { | |
| const { observations } = normalizeObservations(missionId); | |
| return observations.filter(o => o.phase === phase && o.status === "success"); | |
| } |