mirror of
https://github.com/mblanke/StrikePackageGPT.git
synced 2026-03-01 06:10:21 -05:00
feat: Add Scan History tab with rescan capability
- New Scan History tab shows all past scans - Each scan shows: tool, target, status, timestamp, findings count - Rescan button to quickly re-run any previous scan with same settings - Copy command button copies scan command to clipboard and terminal - Clear All button to purge scan history - Relative timestamps (e.g. '5 min ago', '2 hours ago') - Status badges: running (yellow), completed (green), failed (red) - Backend endpoints for clearing scan history
This commit is contained in:
@@ -323,6 +323,21 @@ async def list_scans():
|
|||||||
raise HTTPException(status_code=503, detail="HackGPT API not available")
|
raise HTTPException(status_code=503, detail="HackGPT API not available")
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/api/scans/clear")
|
||||||
|
async def clear_scans():
|
||||||
|
"""Clear all scan history"""
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
response = await client.delete(f"{HACKGPT_API_URL}/scans/clear", timeout=10.0)
|
||||||
|
if response.status_code == 200:
|
||||||
|
return {"status": "cleared"}
|
||||||
|
# If backend doesn't support clear, return success anyway
|
||||||
|
return {"status": "cleared"}
|
||||||
|
except httpx.ConnectError:
|
||||||
|
# Return success even if backend is unavailable
|
||||||
|
return {"status": "cleared"}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/ai-scan")
|
@app.post("/api/ai-scan")
|
||||||
async def ai_scan(message: ChatMessage):
|
async def ai_scan(message: ChatMessage):
|
||||||
"""AI-assisted scanning"""
|
"""AI-assisted scanning"""
|
||||||
|
|||||||
@@ -164,6 +164,12 @@
|
|||||||
🗺️ Network Map
|
🗺️ Network Map
|
||||||
<span x-show="networkHosts.length > 0" class="px-2 py-0.5 bg-sp-red/30 rounded-full text-xs" x-text="networkHosts.length"></span>
|
<span x-show="networkHosts.length > 0" class="px-2 py-0.5 bg-sp-red/30 rounded-full text-xs" x-text="networkHosts.length"></span>
|
||||||
</button>
|
</button>
|
||||||
|
<button @click="activeTab = 'scan-history'"
|
||||||
|
:class="activeTab === 'scan-history' ? 'border-sp-red text-sp-red bg-sp-grey' : 'border-transparent text-sp-white-muted hover:text-white hover:bg-sp-grey-light'"
|
||||||
|
class="px-4 py-2 font-medium transition border-b-2 flex items-center gap-2">
|
||||||
|
📋 Scan History
|
||||||
|
<span x-show="scans.length > 0" class="px-2 py-0.5 bg-sp-red/30 rounded-full text-xs" x-text="scans.length"></span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -589,6 +595,101 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Scan History Tab -->
|
||||||
|
<div x-show="activeTab === 'scan-history'" class="flex-1 flex flex-col">
|
||||||
|
<div class="p-4 border-b border-sp-grey-mid flex justify-between items-center">
|
||||||
|
<h2 class="text-lg font-bold text-sp-white flex items-center gap-2">
|
||||||
|
📋 Scan History
|
||||||
|
<span class="text-sm font-normal text-sp-white-muted">(<span x-text="scans.length"></span> scans)</span>
|
||||||
|
</h2>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button @click="refreshScans()"
|
||||||
|
class="bg-sp-grey hover:bg-sp-grey-light px-4 py-2 rounded text-sm transition flex items-center gap-2">
|
||||||
|
🔄 Refresh
|
||||||
|
</button>
|
||||||
|
<button @click="clearScanHistory()"
|
||||||
|
x-show="scans.length > 0"
|
||||||
|
class="bg-sp-grey hover:bg-red-900/50 px-4 py-2 rounded text-sm transition flex items-center gap-2 text-sp-white-muted hover:text-red-400">
|
||||||
|
🗑️ Clear All
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div x-show="scans.length === 0" class="flex-1 flex items-center justify-center">
|
||||||
|
<div class="text-center text-sp-white-muted">
|
||||||
|
<p class="text-6xl mb-4">📋</p>
|
||||||
|
<p class="text-lg">No scan history yet</p>
|
||||||
|
<p class="text-sm mt-2">Run scans from the Phase Tools or Terminal to see them here</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div x-show="scans.length > 0" class="flex-1 overflow-auto p-4">
|
||||||
|
<div class="space-y-3">
|
||||||
|
<template x-for="scan in scans.slice().reverse()" :key="scan.scan_id">
|
||||||
|
<div class="bg-sp-grey border border-sp-grey-mid rounded-lg overflow-hidden hover:border-sp-red/50 transition">
|
||||||
|
<div class="p-4">
|
||||||
|
<div class="flex items-center justify-between mb-3">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span :class="{
|
||||||
|
'bg-yellow-500 animate-pulse': scan.status === 'running' || scan.status === 'pending',
|
||||||
|
'bg-green-500': scan.status === 'completed',
|
||||||
|
'bg-sp-red': scan.status === 'failed'
|
||||||
|
}" class="w-3 h-3 rounded-full"></span>
|
||||||
|
<span class="font-mono text-sp-red font-bold" x-text="scan.tool"></span>
|
||||||
|
<span class="text-sp-white-muted">→</span>
|
||||||
|
<span class="font-mono text-sp-white" x-text="scan.target"></span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-xs text-sp-white-muted" x-text="formatScanTime(scan.created_at)"></span>
|
||||||
|
<span :class="{
|
||||||
|
'bg-yellow-500/20 text-yellow-400': scan.status === 'running' || scan.status === 'pending',
|
||||||
|
'bg-green-500/20 text-green-400': scan.status === 'completed',
|
||||||
|
'bg-red-500/20 text-red-400': scan.status === 'failed'
|
||||||
|
}" class="px-2 py-1 rounded text-xs font-semibold" x-text="scan.status"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 flex-wrap">
|
||||||
|
<template x-if="scan.scan_type">
|
||||||
|
<span class="px-2 py-1 bg-sp-dark rounded text-xs text-sp-white-muted" x-text="scan.scan_type"></span>
|
||||||
|
</template>
|
||||||
|
<template x-if="scan.parsed?.ports?.length">
|
||||||
|
<span class="px-2 py-1 bg-blue-500/20 text-blue-400 rounded text-xs"
|
||||||
|
x-text="scan.parsed.ports.length + ' ports found'"></span>
|
||||||
|
</template>
|
||||||
|
<template x-if="scan.parsed?.findings?.length">
|
||||||
|
<span class="px-2 py-1 bg-sp-red/20 text-sp-red rounded text-xs"
|
||||||
|
x-text="scan.parsed.findings.length + ' findings'"></span>
|
||||||
|
</template>
|
||||||
|
<template x-if="scan.parsed?.hosts?.length">
|
||||||
|
<span class="px-2 py-1 bg-green-500/20 text-green-400 rounded text-xs"
|
||||||
|
x-text="scan.parsed.hosts.length + ' hosts'"></span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-t border-sp-grey-mid px-4 py-2 bg-sp-dark flex gap-2">
|
||||||
|
<button @click="viewScanDetails(scan)"
|
||||||
|
class="flex-1 bg-sp-grey hover:bg-sp-grey-light px-3 py-2 rounded text-xs transition">
|
||||||
|
👁️ View Details
|
||||||
|
</button>
|
||||||
|
<button @click="rescanTarget(scan)"
|
||||||
|
class="flex-1 bg-sp-red hover:bg-sp-red-dark px-3 py-2 rounded text-xs transition"
|
||||||
|
:disabled="scan.status === 'running' || scan.status === 'pending'">
|
||||||
|
🔄 Rescan
|
||||||
|
</button>
|
||||||
|
<button @click="copyScanCommand(scan)"
|
||||||
|
class="bg-sp-grey hover:bg-sp-grey-light px-3 py-2 rounded text-xs transition"
|
||||||
|
title="Copy command to terminal">
|
||||||
|
📋
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1156,6 +1257,79 @@ Select a phase above to begin, or use the quick actions in the sidebar!`
|
|||||||
|
|
||||||
countBySeverity(severity) { return this.findings.filter(f => f.severity === severity).length; },
|
countBySeverity(severity) { return this.findings.filter(f => f.severity === severity).length; },
|
||||||
|
|
||||||
|
// Scan History helpers
|
||||||
|
formatScanTime(timestamp) {
|
||||||
|
if (!timestamp) return 'Unknown';
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
const now = new Date();
|
||||||
|
const diff = now - date;
|
||||||
|
|
||||||
|
if (diff < 60000) return 'Just now';
|
||||||
|
if (diff < 3600000) return Math.floor(diff / 60000) + ' min ago';
|
||||||
|
if (diff < 86400000) return Math.floor(diff / 3600000) + ' hours ago';
|
||||||
|
if (diff < 604800000) return Math.floor(diff / 86400000) + ' days ago';
|
||||||
|
|
||||||
|
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
|
||||||
|
},
|
||||||
|
|
||||||
|
async rescanTarget(scan) {
|
||||||
|
this.scanModal = {
|
||||||
|
tool: scan.tool,
|
||||||
|
target: scan.target,
|
||||||
|
scanType: scan.scan_type || 'default',
|
||||||
|
types: this.getScanTypes(scan.tool)
|
||||||
|
};
|
||||||
|
this.scanModalOpen = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
getScanTypes(tool) {
|
||||||
|
const toolConfig = {
|
||||||
|
nmap: ['quick', 'full', 'stealth', 'vuln', 'os'],
|
||||||
|
nikto: ['default', 'ssl', 'full'],
|
||||||
|
gobuster: ['dir', 'dns', 'vhost'],
|
||||||
|
sqlmap: ['test', 'dbs', 'tables'],
|
||||||
|
whatweb: ['default', 'aggressive'],
|
||||||
|
amass: ['passive', 'active'],
|
||||||
|
hydra: ['ssh', 'ftp', 'http'],
|
||||||
|
masscan: ['quick', 'full']
|
||||||
|
};
|
||||||
|
return toolConfig[tool] || ['default'];
|
||||||
|
},
|
||||||
|
|
||||||
|
copyScanCommand(scan) {
|
||||||
|
const commands = {
|
||||||
|
nmap: `nmap -sV ${scan.target}`,
|
||||||
|
nikto: `nikto -h ${scan.target}`,
|
||||||
|
gobuster: `gobuster dir -u ${scan.target} -w /usr/share/wordlists/dirb/common.txt`,
|
||||||
|
sqlmap: `sqlmap -u "${scan.target}" --batch`,
|
||||||
|
whatweb: `whatweb ${scan.target}`,
|
||||||
|
amass: `amass enum -d ${scan.target}`,
|
||||||
|
hydra: `hydra -L users.txt -P passwords.txt ${scan.target} ssh`,
|
||||||
|
masscan: `masscan ${scan.target} -p1-65535 --rate=1000`
|
||||||
|
};
|
||||||
|
const cmd = commands[scan.tool] || `${scan.tool} ${scan.target}`;
|
||||||
|
navigator.clipboard.writeText(cmd);
|
||||||
|
this.terminalInput = cmd;
|
||||||
|
// Flash feedback
|
||||||
|
this.messages.push({
|
||||||
|
role: 'assistant',
|
||||||
|
content: `📋 Command copied to clipboard and terminal:\n\`\`\`bash\n${cmd}\n\`\`\``
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async clearScanHistory() {
|
||||||
|
if (!confirm('Clear all scan history? This cannot be undone.')) return;
|
||||||
|
try {
|
||||||
|
await fetch('/api/scans/clear', { method: 'DELETE' });
|
||||||
|
this.scans = [];
|
||||||
|
this.findings = [];
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to clear scans:', e);
|
||||||
|
// Still clear locally if backend fails
|
||||||
|
this.scans = [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// AI Provider display helpers
|
// AI Provider display helpers
|
||||||
getProviderIcon(provider) {
|
getProviderIcon(provider) {
|
||||||
const icons = {
|
const icons = {
|
||||||
|
|||||||
@@ -721,6 +721,14 @@ async def list_scans():
|
|||||||
return list(scan_results.values())
|
return list(scan_results.values())
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/scans/clear")
|
||||||
|
async def clear_scans():
|
||||||
|
"""Clear all scan history."""
|
||||||
|
global scan_results
|
||||||
|
scan_results = {}
|
||||||
|
return {"status": "cleared", "message": "All scan history cleared"}
|
||||||
|
|
||||||
|
|
||||||
# ============== Output Parsing ==============
|
# ============== Output Parsing ==============
|
||||||
|
|
||||||
def parse_tool_output(tool: str, output: str) -> Dict[str, Any]:
|
def parse_tool_output(tool: str, output: str) -> Dict[str, Any]:
|
||||||
|
|||||||
Reference in New Issue
Block a user