import fs from "fs"; import { resolve } from "path"; import { homedir } from "os"; const MISSIONS_DIR = resolve(homedir(), ".config", "phantom", "missions"); export function createMission(program, scope) { const id = `mission-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; const now = new Date().toISOString(); return { id, programName: program.name || program.handle, programHandle: program.handle, programUrl: program.url, scope, status: "planning", objectives: [ "Passive reconnaissance of in-scope assets", "Active HTTP probing and technology detection", "Vulnerability validation and reporting" ], restrictions: [], createdAt: now, updatedAt: now, activityLog: [{ timestamp: now, action: "created", details: `Mission created for ${program.name || program.handle} (${program.handle})` }] }; } export function saveMission(mission) { if (!fs.existsSync(MISSIONS_DIR)) fs.mkdirSync(MISSIONS_DIR, { recursive: true }); const f = resolve(MISSIONS_DIR, `${mission.id}.json`); fs.writeFileSync(f, JSON.stringify(mission, null, 2), "utf-8"); } export function loadMission(missionId) { const f = resolve(MISSIONS_DIR, `${missionId}.json`); if (!fs.existsSync(f)) return null; return JSON.parse(fs.readFileSync(f, "utf-8")); } export function listMissions() { if (!fs.existsSync(MISSIONS_DIR)) return []; return fs.readdirSync(MISSIONS_DIR) .filter(f => f.endsWith(".json")) .map(f => { const m = JSON.parse(fs.readFileSync(resolve(MISSIONS_DIR, f), "utf-8")); return { id: m.id, programName: m.programName, programHandle: m.programHandle, status: m.status, createdAt: m.createdAt, updatedAt: m.updatedAt, _inScope: (m?.scope?.inScope || []).length }; }).sort((a,b)=> (b._inScope - a._inScope) || String(b.createdAt).localeCompare(String(a.createdAt))) .map(({_inScope,...rest})=>rest); } export function updateMissionStatus(missionId, status) { const mission = loadMission(missionId); if (!mission) return null; mission.status = status; mission.updatedAt = new Date().toISOString(); saveMission(mission); return mission; } export function addMissionActivity(missionId, activity) { const mission = loadMission(missionId); if (!mission) return; mission.activityLog = mission.activityLog || []; mission.activityLog.push({ timestamp: new Date().toISOString(), ...activity }); saveMission(mission); } export function isInScope(target, mission) { const normalized = normalizeTarget(target); for (const scopeEntry of mission.scope.inScope) { if (scopeEntry.isWildcard) { const base = scopeEntry.normalized.replace(/^\*\./, ""); if (normalized === base || normalized.endsWith("." + base)) { return { allowed: true, reason: "Matches wildcard scope", matchingScopeEntry: scopeEntry }; } } else if (normalized === scopeEntry.normalized) { return { allowed: true, reason: "Exact match", matchingScopeEntry: scopeEntry }; } } return { allowed: false, reason: "Not in scope", matchingScopeEntry: null }; } export function normalizeTarget(target) { return target .replace(/^https?:\/\//, "") .replace(/\/.*$/, "") .toLowerCase() .trim(); } export function generateReconPlan(mission) { const targets = mission.scope.inScope.map(s => s.normalized); return { phases: [ { phase: 1, name: "Passive Reconnaissance", description: "Subdomain enumeration, DNS resolution", targets, tools: ["subfinder", "dnsx"] }, { phase: 2, name: "Active HTTP Probing", description: "HTTP probing, technology detection", targets, tools: ["httpx"] }, { phase: 3, name: "Validation & Reporting", description: "Validate findings, generate report", targets, tools: ["nuclei", "custom"] } ], notes: [ "Stage 1 only — NO active exploitation", "Default deny for all targets not in scope", "All findings saved per-target per-phase" ] }; } export async function fetchProgramAndScope(handle) { const { hackerTools } = await import("./tools.mjs"); const result = await hackerTools.hackerone(`scope ${handle}`); const inScope = []; const outScope = []; // The result has literal \n (backslash-n) as line separators between scope entries, // and actual newlines within multi-line descriptions. // First split by literal \n to get the logical lines. const lines = result.split("\\n"); let current = "in"; for (const line of lines) { const trimmed = line.trim(); // Check for section headers on the trimmed line if (trimmed.includes("IN SCOPE")) current = "in"; else if (trimmed.includes("OUT OF SCOPE")) current = "out"; // Check for scope entries on the raw line (which preserves leading spaces) else if (line.startsWith(" ") && trimmed.includes("(")) { // Match: "identifier (TYPE) — description" (trimmed has no leading spaces) const match = trimmed.match(/([^\s]+)\s+\(([^)]+)\)/); if (match) { const [, identifier, assetType] = match; const entry = { identifier, normalized: identifier.replace(/^\*\./, "").toLowerCase(), assetType, isWildcard: identifier.startsWith("*."), maxSeverity: "critical", instruction: current === "out" ? "Not eligible" : "" }; if (current === "in") inScope.push(entry); else outScope.push(entry); } } } // Offline/degraded fallback: when HackerOne creds/network are unavailable, // keep the agent usable with a minimal scoped asset derived from the handle. if (inScope.length === 0) { const fallbackHost = `${handle}.com`; inScope.push({ identifier: fallbackHost, normalized: fallbackHost, assetType: "URL", isWildcard: false, maxSeverity: "critical", instruction: "Offline fallback scope", asset: fallbackHost }); inScope.push({ identifier: `*.${fallbackHost}`, normalized: fallbackHost, assetType: "URL", isWildcard: true, maxSeverity: "critical", instruction: "Offline fallback scope", asset: fallbackHost }); } return { program: { handle, name: handle, url: `https://hackerone.com/${handle}` }, scope: { inScope, exclusions: outScope, restrictions: [] } }; }