Autarch/web/templates/net_mapper.html

265 lines
12 KiB
HTML
Raw Normal View History

{% extends "base.html" %}
{% block title %}Net Mapper — AUTARCH{% endblock %}
{% block content %}
<div class="page-header">
<h1>Network Topology Mapper</h1>
<p class="text-muted">Host discovery, service enumeration, topology visualization</p>
</div>
<div class="tabs">
<button class="tab active" onclick="switchTab('discover')">Discover</button>
<button class="tab" onclick="switchTab('map')">Map</button>
<button class="tab" onclick="switchTab('scans')">Saved Scans</button>
</div>
<!-- Discover -->
<div id="tab-discover" class="tab-content active">
<div class="card" style="max-width:700px">
<h3>Host Discovery</h3>
<div style="display:flex;gap:0.5rem;align-items:end">
<div class="form-group" style="flex:1;margin:0">
<label>Target (CIDR, range, or single IP)</label>
<input type="text" id="disc-target" class="form-control" placeholder="192.168.1.0/24">
</div>
<select id="disc-method" class="form-control" style="width:130px">
<option value="auto">Auto</option>
<option value="nmap">Nmap</option>
<option value="icmp">ICMP/TCP</option>
</select>
<button class="btn btn-primary" id="disc-btn" onclick="startDiscover()">Scan</button>
</div>
<div id="disc-status" style="margin-top:0.5rem"></div>
</div>
<div id="disc-results" style="margin-top:1rem">
<div id="disc-empty" class="card" style="text-align:center;color:var(--text-muted)">Run a discovery scan to find hosts</div>
<div id="disc-hosts" style="display:none">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.5rem">
<h3>Discovered Hosts (<span id="host-count">0</span>)</h3>
<div style="display:flex;gap:0.5rem">
<button class="btn btn-sm" onclick="showTopology()">View Map</button>
<button class="btn btn-sm" onclick="saveScan()">Save Scan</button>
</div>
</div>
<table class="data-table">
<thead><tr><th>IP</th><th>Hostname</th><th>MAC</th><th>OS</th><th>Ports</th><th>Action</th></tr></thead>
<tbody id="hosts-body"></tbody>
</table>
</div>
</div>
</div>
<!-- Map -->
<div id="tab-map" class="tab-content" style="display:none">
<div class="card">
<h3>Network Topology</h3>
<div id="topology-canvas" style="width:100%;height:500px;background:#0a0a12;border-radius:var(--radius);position:relative;overflow:hidden">
<svg id="topo-svg" width="100%" height="100%"></svg>
</div>
<div id="topo-info" style="margin-top:0.5rem;font-size:0.8rem;color:var(--text-muted)"></div>
</div>
</div>
<!-- Saved Scans -->
<div id="tab-scans" class="tab-content" style="display:none">
<h3>Saved Scans</h3>
<div id="scans-list"></div>
<div class="card" style="margin-top:1rem">
<h4>Compare Scans</h4>
<div style="display:flex;gap:0.5rem;align-items:end">
<select id="diff-s1" class="form-control" style="flex:1"></select>
<span>vs</span>
<select id="diff-s2" class="form-control" style="flex:1"></select>
<button class="btn btn-primary" onclick="diffScans()">Compare</button>
</div>
<div id="diff-results" style="margin-top:0.5rem"></div>
</div>
</div>
<style>
.node-circle{cursor:pointer;transition:r 0.2s}
.node-circle:hover{r:12}
.spinner-inline{display:inline-block;width:14px;height:14px;border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin 0.8s linear infinite;vertical-align:middle;margin-right:6px}
@keyframes spin{to{transform:rotate(360deg)}}
</style>
<script>
let currentHosts=[];
let discPoll=null;
function switchTab(name){
document.querySelectorAll('.tab').forEach((t,i)=>t.classList.toggle('active',['discover','map','scans'][i]===name));
document.querySelectorAll('.tab-content').forEach(c=>c.style.display='none');
document.getElementById('tab-'+name).style.display='block';
if(name==='scans') loadScans();
}
function startDiscover(){
const target=document.getElementById('disc-target').value.trim();
if(!target) return;
document.getElementById('disc-btn').disabled=true;
document.getElementById('disc-status').innerHTML='<div class="spinner-inline"></div> Discovering hosts...';
fetch('/net-mapper/discover',{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({target,method:document.getElementById('disc-method').value})})
.then(r=>r.json()).then(d=>{
if(!d.ok){showDiscError(d.error);return}
if(discPoll) clearInterval(discPoll);
discPoll=setInterval(()=>{
fetch('/net-mapper/discover/'+d.job_id).then(r=>r.json()).then(s=>{
if(!s.done) return;
clearInterval(discPoll);discPoll=null;
document.getElementById('disc-btn').disabled=false;
document.getElementById('disc-status').innerHTML='';
currentHosts=s.hosts||[];
renderHosts(currentHosts);
});
},2000);
}).catch(e=>showDiscError(e.message));
}
function showDiscError(msg){
document.getElementById('disc-btn').disabled=false;
document.getElementById('disc-status').innerHTML='<span style="color:var(--danger)">'+esc(msg)+'</span>';
}
function renderHosts(hosts){
document.getElementById('disc-empty').style.display='none';
document.getElementById('disc-hosts').style.display='';
document.getElementById('host-count').textContent=hosts.length;
const body=document.getElementById('hosts-body');
body.innerHTML=hosts.map(h=>`<tr>
<td><strong>${esc(h.ip)}</strong></td>
<td>${esc(h.hostname||'—')}</td>
<td style="font-size:0.8rem">${esc(h.mac||'—')}</td>
<td style="font-size:0.8rem">${esc(h.os_guess||'—')}</td>
<td>${(h.ports||[]).length}</td>
<td><button class="btn btn-sm" onclick="scanHost('${h.ip}')">Detail Scan</button></td>
</tr>`).join('');
}
function scanHost(ip){
fetch('/net-mapper/scan-host',{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({ip})})
.then(r=>r.json()).then(d=>{
if(!d.ok){alert(d.error);return}
const h=d.host;
let msg=`${h.ip} — ${h.os_guess||'unknown OS'}\n\nOpen Ports:\n`;
(h.ports||[]).forEach(p=>{msg+=` ${p.port}/${p.protocol} ${p.service||''} ${p.version||''}\n`});
alert(msg);
// Update in current hosts
const idx=currentHosts.findIndex(x=>x.ip===ip);
if(idx>=0) currentHosts[idx]=h;
});
}
function showTopology(){
if(!currentHosts.length) return;
fetch('/net-mapper/topology',{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({hosts:currentHosts})})
.then(r=>r.json()).then(d=>{
if(!d.ok) return;
renderTopology(d);
switchTab('map');
});
}
function renderTopology(data){
const svg=document.getElementById('topo-svg');
const w=svg.clientWidth||800,h=svg.clientHeight||500;
svg.innerHTML='';
const nodes=data.nodes.filter(n=>n.type!=='subnet');
const colors={host:'#6366f1',web:'#22c55e',server:'#f59e0b',windows:'#3b82f6',subnet:'#444'};
// Simple force-directed-ish layout
nodes.forEach((n,i)=>{
const angle=(i/nodes.length)*Math.PI*2;
const radius=Math.min(w,h)*0.35;
n.x=w/2+Math.cos(angle)*radius;
n.y=h/2+Math.sin(angle)*radius;
});
// Edges
data.edges.forEach(e=>{
const from=nodes.find(n=>n.id===e.from);
const to=nodes.find(n=>n.id===e.to);
if(from&&to){
const line=document.createElementNS('http://www.w3.org/2000/svg','line');
line.setAttribute('x1',from.x);line.setAttribute('y1',from.y);
line.setAttribute('x2',to.x);line.setAttribute('y2',to.y);
line.setAttribute('stroke','#333');line.setAttribute('stroke-width','1');
svg.appendChild(line);
}
});
// Nodes
nodes.forEach(n=>{
const g=document.createElementNS('http://www.w3.org/2000/svg','g');
const circle=document.createElementNS('http://www.w3.org/2000/svg','circle');
circle.setAttribute('cx',n.x);circle.setAttribute('cy',n.y);
circle.setAttribute('r','8');circle.setAttribute('fill',colors[n.type]||'#6366f1');
circle.classList.add('node-circle');
const text=document.createElementNS('http://www.w3.org/2000/svg','text');
text.setAttribute('x',n.x);text.setAttribute('y',n.y+20);
text.setAttribute('text-anchor','middle');text.setAttribute('fill','#888');
text.setAttribute('font-size','10');text.textContent=n.label||n.ip;
g.appendChild(circle);g.appendChild(text);svg.appendChild(g);
});
document.getElementById('topo-info').textContent=`${nodes.length} hosts across ${data.subnets.length} subnet(s)`;
}
function saveScan(){
if(!currentHosts.length) return;
const name=prompt('Scan name:');
if(!name) return;
fetch('/net-mapper/scans',{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({name,hosts:currentHosts})})
.then(r=>r.json()).then(d=>{if(d.ok) alert('Scan saved')});
}
function loadScans(){
fetch('/net-mapper/scans').then(r=>r.json()).then(d=>{
const scans=d.scans||[];
const list=document.getElementById('scans-list');
const s1=document.getElementById('diff-s1'),s2=document.getElementById('diff-s2');
s1.innerHTML='';s2.innerHTML='';
if(!scans.length){list.innerHTML='<div class="card" style="color:var(--text-muted)">No saved scans</div>';return}
list.innerHTML=scans.map(s=>`<div class="card" style="margin-bottom:0.5rem;cursor:pointer" onclick="loadSavedScan('${esc(s.file)}')">
<div style="display:flex;justify-content:space-between"><div><strong>${esc(s.name)}</strong> — ${s.host_count} hosts</div>
<span style="font-size:0.8rem;color:var(--text-muted)">${(s.timestamp||'').slice(0,19)}</span></div></div>`).join('');
scans.forEach(s=>{
s1.innerHTML+=`<option value="${esc(s.file)}">${esc(s.name)} (${s.host_count})</option>`;
s2.innerHTML+=`<option value="${esc(s.file)}">${esc(s.name)} (${s.host_count})</option>`;
});
});
}
function loadSavedScan(file){
fetch('/net-mapper/scans/'+encodeURIComponent(file)).then(r=>r.json()).then(d=>{
if(!d.ok) return;
currentHosts=d.scan.hosts||[];
renderHosts(currentHosts);
switchTab('discover');
});
}
function diffScans(){
const s1=document.getElementById('diff-s1').value;
const s2=document.getElementById('diff-s2').value;
if(!s1||!s2) return;
fetch('/net-mapper/diff',{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({scan1:s1,scan2:s2})})
.then(r=>r.json()).then(d=>{
if(!d.ok){document.getElementById('diff-results').innerHTML=esc(d.error);return}
let html=`<div style="margin-top:0.5rem;font-size:0.85rem">`;
html+=`<div style="color:#22c55e"><strong>+ New hosts (${d.new_hosts.length}):</strong> ${d.new_hosts.join(', ')||'none'}</div>`;
html+=`<div style="color:var(--danger)"><strong>- Removed (${d.removed_hosts.length}):</strong> ${d.removed_hosts.join(', ')||'none'}</div>`;
html+=`<div style="color:var(--text-muted)">Unchanged: ${d.unchanged_hosts.length}</div></div>`;
document.getElementById('diff-results').innerHTML=html;
});
}
function esc(s){return s?String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'):''}
</script>
{% endblock %}