265 lines
12 KiB
HTML
265 lines
12 KiB
HTML
|
|
{% 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='';
|
||
|
|
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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'):''}
|
||
|
|
</script>
|
||
|
|
{% endblock %}
|