Download lib/recon.mjs from teddyoas/Asnip: direct link, hf CLI and curl.
- Browser
- Download file 17.2 kB
-
https://huggingface.co/teddyoas/Asnip/resolve/main/lib/recon.mjs
- Command line
-
hf download hf://teddyoas/Asnip/lib/recon.mjs
-
curl -L -o recon.mjs https://huggingface.co/teddyoas/Asnip/resolve/main/lib/recon.mjs
17.2 kB
| import fs from "fs"; | |
| import { resolve } from "path"; | |
| import { homedir } from "os"; | |
| import { execFileSync } from "child_process"; | |
| import { BASE_DIR, REPORTS_DIR, MISSIONS_DIR } from "./config.mjs"; | |
| import { isInScope, createMission, saveMission, loadMission, listMissions, updateMissionStatus, addMissionActivity, generateReconPlan, fetchProgramAndScope } from "./mission.mjs"; | |
| const EXEC_STATES = { | |
| PLANNING: "planning", | |
| READY: "ready", | |
| RECONNAISSANCE: "reconnaissance", | |
| PAUSED: "paused", | |
| FAILED: "failed", | |
| COMPLETED: "completed", | |
| CANCELLED: "cancelled" | |
| }; | |
| const VALID_TRANSITIONS = { | |
| [EXEC_STATES.PLANNING]: [EXEC_STATES.READY], | |
| [EXEC_STATES.READY]: [EXEC_STATES.RECONNAISSANCE], | |
| [EXEC_STATES.RECONNAISSANCE]: [EXEC_STATES.PAUSED, EXEC_STATES.FAILED, EXEC_STATES.COMPLETED, EXEC_STATES.CANCELLED], | |
| [EXEC_STATES.PAUSED]: [EXEC_STATES.RECONNAISSANCE, EXEC_STATES.CANCELLED], | |
| [EXEC_STATES.FAILED]: [EXEC_STATES.RECONNAISSANCE, EXEC_STATES.CANCELLED], | |
| [EXEC_STATES.COMPLETED]: [], | |
| [EXEC_STATES.CANCELLED]: [] | |
| }; | |
| function getMissionDir(missionId) { | |
| return resolve(MISSIONS_DIR, missionId); | |
| } | |
| function getExecutionStateFile(missionId) { | |
| return resolve(getMissionDir(missionId), "state.json"); | |
| } | |
| function getExecutionState(missionId) { | |
| const f = getExecutionStateFile(missionId); | |
| if (!fs.existsSync(f)) return null; | |
| try { return JSON.parse(fs.readFileSync(f, "utf-8")).state; } catch { return null; } | |
| } | |
| function setExecutionState(missionId, state) { | |
| const dir = getMissionDir(missionId); | |
| if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); | |
| fs.writeFileSync(getExecutionStateFile(missionId), JSON.stringify({ state, updatedAt: new Date().toISOString() }), "utf-8"); | |
| } | |
| function transitionState(missionId, newState) { | |
| const current = getExecutionState(missionId); | |
| if (!current) throw new Error(`Mission ${missionId} has no execution state`); | |
| const allowed = VALID_TRANSITIONS[current] || []; | |
| if (!allowed.includes(newState)) throw new Error(`Invalid transition: ${current} โ ${newState}`); | |
| setExecutionState(missionId, newState); | |
| return { state: newState, previous: current }; | |
| } | |
| function ensureMissionDirs(missionId) { | |
| const dir = getMissionDir(missionId); | |
| const phaseDirs = ["phase1_passive", "phase2_active", "phase3_validation", "errors"]; | |
| if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); | |
| for (const d of phaseDirs) { | |
| if (!fs.existsSync(resolve(dir, d))) fs.mkdirSync(resolve(dir, d), { recursive: true }); | |
| } | |
| return dir; | |
| } | |
| function getResultKey(phase, tool, target) { | |
| return `${phase}_${tool}_${target.replace(/[^a-zA-Z0-9.-]/g, "_")}`; | |
| } | |
| function resultExists(missionId, key) { | |
| const dir = getMissionDir(missionId); | |
| const files = fs.readdirSync(dir, { recursive: true }).filter(f => f.endsWith(".json")); | |
| return files.some(f => f.includes(key)); | |
| } | |
| function saveResult(missionId, key, data) { | |
| const dir = getMissionDir(missionId); | |
| fs.writeFileSync(resolve(dir, `${key}.json`), JSON.stringify({ ...data, savedAt: new Date().toISOString() }, null, 2), "utf-8"); | |
| } | |
| function loadExistingResult(missionId, key) { | |
| const dir = getMissionDir(missionId); | |
| const f = resolve(dir, `${key}.json`); | |
| if (!fs.existsSync(f)) return null; | |
| return JSON.parse(fs.readFileSync(f, "utf-8")); | |
| } | |
| function saveError(missionId, phase, tool, target, error) { | |
| const dir = resolve(getMissionDir(missionId), "errors"); | |
| if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); | |
| const key = `${phase}_${tool}_${target.replace(/[^a-zA-Z0-9.-]/g, "_")}_error.json`; | |
| fs.writeFileSync(resolve(dir, key), JSON.stringify({ phase, tool, target, error: String(error), timestamp: new Date().toISOString() }, null, 2), "utf-8"); | |
| } | |
| function appendActivity(missionId, activity) { | |
| const dir = getMissionDir(missionId); | |
| const f = resolve(dir, "activity.jsonl"); | |
| const entry = { timestamp: new Date().toISOString(), ...activity }; | |
| fs.appendFileSync(f, JSON.stringify(entry) + "\n", "utf-8"); | |
| } | |
| async function startRecon(missionId) { | |
| const mission = loadMission(missionId); | |
| if (!mission) throw new Error(`Mission not found: ${missionId}`); | |
| ensureMissionDirs(missionId); | |
| // Initialize execution state if not exists | |
| const currentState = getExecutionState(missionId); | |
| if (!currentState) { | |
| setExecutionState(missionId, EXEC_STATES.READY); | |
| } | |
| // If already completed, allow re-run by resetting to READY first | |
| else if (currentState === EXEC_STATES.COMPLETED) { | |
| setExecutionState(missionId, EXEC_STATES.READY); | |
| } | |
| transitionState(missionId, EXEC_STATES.RECONNAISSANCE); | |
| updateMissionStatus(missionId, "reconnaissance"); | |
| appendActivity(missionId, { action: "recon_start", phase: 1 }); | |
| // Phase 1: Passive recon | |
| const findings = []; | |
| const inScopeTargets = mission.scope.inScope.map(s => s.normalized); | |
| for (const target of inScopeTargets) { | |
| if (getExecutionState(missionId) !== EXEC_STATES.RECONNAISSANCE) break; | |
| try { | |
| const { subfinder, dnsx, httpx } = await import("./tools.mjs"); | |
| const subResult = await subfinder(target); | |
| saveResult(missionId, getResultKey(1, "subfinder", target), { target, tool: "subfinder", result: subResult, phase: 1 }); | |
| appendActivity(missionId, { action: "tool_complete", phase: 1, tool: "subfinder", target }); | |
| // Extract subdomains and run dnsx | |
| const subs = subResult.split("\n").filter(l => l.trim() && !l.startsWith("[")).slice(0, 50); | |
| if (subs.length > 0) { | |
| const dnsResult = await dnsx(subs.join(" ")); | |
| saveResult(missionId, getResultKey(1, "dnsx", target), { target, tool: "dnsx", result: dnsResult, phase: 1 }); | |
| appendActivity(missionId, { action: "tool_complete", phase: 1, tool: "dnsx", target }); | |
| } | |
| findings.push({ target, phase: 1, status: "success", tools: ["subfinder", "dnsx"] }); | |
| } catch (e) { | |
| saveError(missionId, 1, "subfinder", target, e); | |
| findings.push({ target, phase: 1, status: "error", error: String(e) }); | |
| } | |
| } | |
| // Phase 2: Active recon (httpx) | |
| if (getExecutionState(missionId) === EXEC_STATES.RECONNAISSANCE) { | |
| appendActivity(missionId, { action: "phase_start", phase: 2 }); | |
| for (const target of inScopeTargets) { | |
| if (getExecutionState(missionId) !== EXEC_STATES.RECONNAISSANCE) break; | |
| try { | |
| const { httpx } = await import("./tools.mjs"); | |
| const httpResult = await httpx(target); | |
| saveResult(missionId, getResultKey(2, "httpx", target), { target, tool: "httpx", result: httpResult, phase: 2 }); | |
| appendActivity(missionId, { action: "tool_complete", phase: 2, tool: "httpx", target }); | |
| findings.push({ target, phase: 2, status: "success", tools: ["httpx"] }); | |
| } catch (e) { | |
| saveError(missionId, 2, "httpx", target, e); | |
| findings.push({ target, phase: 2, status: "error", error: String(e) }); | |
| } | |
| } | |
| } | |
| // Phase 3: Validation | |
| if (getExecutionState(missionId) === EXEC_STATES.RECONNAISSANCE) { | |
| appendActivity(missionId, { action: "phase_start", phase: 3 }); | |
| transitionState(missionId, EXEC_STATES.COMPLETED); | |
| updateMissionStatus(missionId, "completed"); | |
| appendActivity(missionId, { action: "recon_complete" }); | |
| // Generate learning from completed mission (non-blocking, best-effort) | |
| try { | |
| const { recordReconLearning, loadLearning } = await import("./recon-learning.mjs"); | |
| // Check if learning already exists for this mission (deduplication) | |
| const existing = loadLearning({ missionId, type: "recon_learning" }); | |
| if (existing.length === 0) { | |
| const mission = loadMission(missionId); | |
| if (mission) { | |
| // Calculate duration from activity log | |
| const activityFile = resolve(getMissionDir(missionId), "activity.jsonl"); | |
| let duration = null; | |
| if (fs.existsSync(activityFile)) { | |
| const lines = fs.readFileSync(activityFile, "utf-8").trim().split("\n").filter(Boolean); | |
| if (lines.length >= 2) { | |
| const first = JSON.parse(lines[0]); | |
| const last = JSON.parse(lines[lines.length - 1]); | |
| duration = new Date(last.timestamp) - new Date(first.timestamp); | |
| } | |
| } | |
| // Collect successful techniques from phase directories | |
| const successfulTechniques = []; | |
| const toolFailures = []; | |
| const discoveryMethods = []; | |
| const phaseResults = {}; | |
| const phaseDirs = ["phase1_passive", "phase2_active", "phase3_validation"]; | |
| for (const phaseDir of phaseDirs) { | |
| const dir = resolve(getMissionDir(missionId), phaseDir); | |
| if (fs.existsSync(dir)) { | |
| const files = fs.readdirSync(dir).filter(f => f.endsWith(".json")); | |
| const phaseNum = parseInt(phaseDir.match(/\d+/)?.[0] || "0"); | |
| phaseResults[`phase${phaseNum}`] = files.length; | |
| for (const file of files) { | |
| try { | |
| const content = JSON.parse(fs.readFileSync(resolve(dir, file), "utf-8")); | |
| if (content.tool && !successfulTechniques.includes(content.tool)) { | |
| successfulTechniques.push(content.tool); | |
| } | |
| // Map tools to discovery methods | |
| if (content.tool === "subfinder" && !discoveryMethods.includes("passive_subdomain_enum")) { | |
| discoveryMethods.push("passive_subdomain_enum"); | |
| } | |
| if (content.tool === "dnsx" && !discoveryMethods.includes("dns_resolution")) { | |
| discoveryMethods.push("dns_resolution"); | |
| } | |
| if (content.tool === "httpx" && !discoveryMethods.includes("http_probing")) { | |
| discoveryMethods.push("http_probing"); | |
| } | |
| if (content.tool === "nuclei" && !discoveryMethods.includes("technology_fingerprinting")) { | |
| discoveryMethods.push("technology_fingerprinting"); | |
| } | |
| } catch {} | |
| } | |
| } | |
| } | |
| // Collect tool failures from errors directory | |
| const errorsDir = resolve(getMissionDir(missionId), "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")); | |
| if (content.tool && content.target) { | |
| toolFailures.push({ tool: content.tool, target: content.target, error: content.error }); | |
| } | |
| } catch {} | |
| } | |
| } | |
| // Detect false positive patterns | |
| const falsePositivePatterns = []; | |
| const nucleiFiles = fs.readdirSync(resolve(getMissionDir(missionId), "phase3_validation") || "") | |
| .filter(f => f.includes("nuclei") && f.endsWith(".json")); | |
| if (nucleiFiles.length > 0 && discoveryMethods.includes("http_probing")) { | |
| const httpFiles = fs.readdirSync(resolve(getMissionDir(missionId), "phase2_active") || "") | |
| .filter(f => f.includes("httpx") && f.endsWith(".json")); | |
| if (nucleiFiles.length > httpFiles.length * 2) { | |
| falsePositivePatterns.push("High nuclei findings relative to live HTTP services"); | |
| } | |
| } | |
| recordReconLearning(missionId, { | |
| successfulTechniques, | |
| toolFailures, | |
| duration, | |
| discoveryMethods, | |
| falsePositivePatterns, | |
| targetCount: mission.scope.inScope.length, | |
| phaseResults | |
| }); | |
| } | |
| } | |
| } catch (e) { | |
| // Non-blocking - learning failure should never affect recon | |
| appendActivity(missionId, { action: "learning_error", error: String(e) }); | |
| } | |
| } | |
| return findings; | |
| } | |
| function pauseRecon(missionId) { | |
| const state = getExecutionState(missionId); | |
| if (state !== EXEC_STATES.RECONNAISSANCE) throw new Error(`Cannot pause: current state is ${state}`); | |
| transitionState(missionId, EXEC_STATES.PAUSED); | |
| updateMissionStatus(missionId, "paused"); | |
| appendActivity(missionId, { action: "recon_pause" }); | |
| return { success: true, state: EXEC_STATES.PAUSED }; | |
| } | |
| async function resumeRecon(missionId) { | |
| const state = getExecutionState(missionId); | |
| if (state !== EXEC_STATES.PAUSED && state !== EXEC_STATES.FAILED) { | |
| throw new Error(`Cannot resume: current state is ${state}`); | |
| } | |
| return await startRecon(missionId); | |
| } | |
| function cancelMission(missionId) { | |
| const state = getExecutionState(missionId); | |
| if (state === EXEC_STATES.COMPLETED || state === EXEC_STATES.CANCELLED) { | |
| throw new Error(`Mission already ${state}`); | |
| } | |
| transitionState(missionId, EXEC_STATES.CANCELLED); | |
| updateMissionStatus(missionId, "cancelled"); | |
| appendActivity(missionId, { action: "mission_cancel" }); | |
| return { success: true, state: EXEC_STATES.CANCELLED }; | |
| } | |
| function stopRecon(missionId) { | |
| const state = getExecutionState(missionId); | |
| if (state !== EXEC_STATES.RECONNAISSANCE) { | |
| return { success: false, message: `No active recon to stop (state: ${state})`, state }; | |
| } | |
| transitionState(missionId, EXEC_STATES.PAUSED); | |
| updateMissionStatus(missionId, "paused"); | |
| appendActivity(missionId, { action: "recon_stop" }); | |
| return { success: true, message: "Reconnaissance stopped, can resume", state: EXEC_STATES.PAUSED }; | |
| } | |
| function getExecutionSummary(missionId) { | |
| const state = getExecutionState(missionId); | |
| if (!state) return { error: "No execution state" }; | |
| const dir = getMissionDir(missionId); | |
| if (!fs.existsSync(dir)) return { state, findings: 0, errors: 0 }; | |
| const files = fs.readdirSync(dir, { recursive: true }); | |
| const results = files.filter(f => f.endsWith(".json") && !f.includes("error") && f !== "state.json").length; | |
| const errors = files.filter(f => f.includes("error")).length; | |
| const activityFile = resolve(dir, "activity.jsonl"); | |
| const activity = fs.existsSync(activityFile) ? fs.readFileSync(activityFile, "utf-8").trim().split("\n").filter(Boolean).length : 0; | |
| return { state, findings: results, errors, activity }; | |
| } | |
| function generateReconSummary(missionId) { | |
| const mission = loadMission(missionId); | |
| if (!mission) return null; | |
| const summary = getExecutionSummary(missionId); | |
| const findings = getMissionFindings(missionId); | |
| let out = `๐ RECON COMPLETE โ ${mission.programName} (${missionId})\n`; | |
| out += `State: ${summary.state}\n`; | |
| out += `Findings: ${summary.findings} | Errors: ${summary.errors} | Activity: ${summary.activity}\n\n`; | |
| out += `๐ PHASE 1 โ Passive Reconnaissance\n`; | |
| const phase1 = findings.filter(f => f.phase === 1); | |
| out += ` Targets processed: ${phase1.length}\n`; | |
| out += ` Successful: ${phase1.filter(f => f.status === "success").length}\n`; | |
| out += ` Errors: ${phase1.filter(f => f.status === "error").length}\n\n`; | |
| out += `๐ PHASE 2 โ Active HTTP Probing\n`; | |
| const phase2 = findings.filter(f => f.phase === 2); | |
| out += ` Targets processed: ${phase2.length}\n`; | |
| out += ` Successful: ${phase2.filter(f => f.status === "success").length}\n\n`; | |
| out += `๐ PHASE 3 โ Validation\n`; | |
| out += ` Completed: ${summary.state === "completed" ? "Yes" : "Pending"}\n\n`; | |
| out += `๐พ Results saved in: ${getMissionDir(missionId)}`; | |
| return out; | |
| } | |
| function getMissionFindings(missionId) { | |
| const dir = getMissionDir(missionId); | |
| if (!fs.existsSync(dir)) return []; | |
| const findings = []; | |
| const files = fs.readdirSync(dir, { recursive: true }).filter(f => f.endsWith(".json") && !f.includes("error") && f !== "state.json"); | |
| for (const f of files) { | |
| try { | |
| const data = JSON.parse(fs.readFileSync(resolve(dir, f), "utf-8")); | |
| if (data.phase) findings.push(data); | |
| } catch {} | |
| } | |
| return findings; | |
| } | |
| function loadMissionResults(missionId) { | |
| const dir = getMissionDir(missionId); | |
| if (!fs.existsSync(dir)) return []; | |
| return fs.readdirSync(dir, { recursive: true }).filter(f => f.endsWith(".json") && !f.includes("error") && f !== "state.json"); | |
| } | |
| function loadMissionErrors(missionId) { | |
| const dir = resolve(getMissionDir(missionId), "errors"); | |
| if (!fs.existsSync(dir)) return []; | |
| return fs.readdirSync(dir).filter(f => f.endsWith(".json")); | |
| } | |
| function loadMissionActivity(missionId) { | |
| const f = resolve(getMissionDir(missionId), "activity.jsonl"); | |
| if (!fs.existsSync(f)) return []; | |
| return fs.readFileSync(f, "utf-8").trim().split("\n").filter(Boolean).map(JSON.parse); | |
| } | |
| export { | |
| EXEC_STATES, | |
| VALID_TRANSITIONS, | |
| startRecon, | |
| pauseRecon, | |
| resumeRecon, | |
| cancelMission, | |
| stopRecon, | |
| getExecutionState, | |
| setExecutionState, | |
| transitionState, | |
| getExecutionSummary, | |
| generateReconSummary, | |
| getMissionFindings, | |
| loadMissionResults, | |
| loadMissionErrors, | |
| loadMissionActivity, | |
| ensureMissionDirs, | |
| getResultKey, | |
| resultExists, | |
| saveResult, | |
| loadExistingResult, | |
| saveError, | |
| appendActivity | |
| }; |