Asnip / lib /recon-analysis.mjs
teddyoas's picture
Duplicate from teddyoas/PHANTOM-v2
20f108c
Raw History Blame Contribute Delete
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");
}