init: NetMap — scanner réseau local avec interface web

Scan nmap de 192.168.1.10-150, affichage IP/MAC/fabricant/hostname,
nommage personnalisé par appareil (JSON), serveur HTTP Python stdlib.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
perco
2026-06-28 11:53:51 +02:00
co-authored by Claude Sonnet 4.6
commit b70c4c923b
2 changed files with 266 additions and 0 deletions
Executable
+262
View File
@@ -0,0 +1,262 @@
#!/usr/bin/env python3
"""NetMap - Scan et nommage des appareils réseau 192.168.1.10-150"""
import json
import os
import re
import subprocess
import sys
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlparse, parse_qs
DATA_FILE = os.path.join(os.path.dirname(__file__), "devices.json")
IP_START = "192.168.1.10"
IP_END = "192.168.1.150"
NMAP_RANGE = f"192.168.1.10-150"
PORT = 8787
def load_devices():
if os.path.exists(DATA_FILE):
with open(DATA_FILE) as f:
return json.load(f)
return {}
def save_devices(data):
with open(DATA_FILE, "w") as f:
json.dump(data, f, indent=2)
def run_scan():
"""Lance nmap et retourne la liste des hôtes détectés."""
try:
result = subprocess.run(
["nmap", "-sn", "-T4", "--host-timeout", "2s", NMAP_RANGE],
capture_output=True, text=True, timeout=120
)
return parse_nmap_output(result.stdout)
except Exception as e:
return {"error": str(e)}
def parse_nmap_output(output):
hosts = []
current = {}
for line in output.splitlines():
m_host = re.search(r"Nmap scan report for (.+)", line)
if m_host:
if current.get("ip"):
hosts.append(current)
raw = m_host.group(1)
# "hostname (ip)" ou juste "ip"
ip_match = re.search(r"\((\d+\.\d+\.\d+\.\d+)\)", raw)
if ip_match:
current = {"ip": ip_match.group(1), "hostname": raw.split("(")[0].strip()}
else:
current = {"ip": raw.strip(), "hostname": ""}
current["mac"] = ""
current["vendor"] = ""
continue
m_mac = re.search(r"MAC Address: ([0-9A-F:]+)\s*(?:\((.+)\))?", line, re.I)
if m_mac:
current["mac"] = m_mac.group(1)
current["vendor"] = m_mac.group(2) or ""
if current.get("ip"):
hosts.append(current)
return hosts
HTML = r"""<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>NetMap — Réseau local</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Segoe UI', system-ui, sans-serif; background: #0f1117; color: #e2e8f0; min-height: 100vh; }
header { background: #1a1d2e; border-bottom: 1px solid #2d3250; padding: 18px 32px; display: flex; align-items: center; gap: 16px; }
header h1 { font-size: 1.4rem; font-weight: 700; color: #7c8cf8; }
header span { font-size: 0.85rem; color: #64748b; }
.container { max-width: 960px; margin: 0 auto; padding: 28px 20px; }
.toolbar { display: flex; gap: 12px; margin-bottom: 24px; align-items: center; flex-wrap: wrap; }
button { background: #7c8cf8; color: #fff; border: none; border-radius: 8px; padding: 9px 20px; font-size: 0.9rem; cursor: pointer; font-weight: 600; transition: background .15s; }
button:hover { background: #6370f0; }
button:disabled { background: #3d4466; color: #8a94b4; cursor: not-allowed; }
.status { font-size: 0.85rem; color: #64748b; }
.status.scanning { color: #f59e0b; }
.status.done { color: #22c55e; }
table { width: 100%; border-collapse: collapse; }
thead th { background: #1a1d2e; padding: 12px 14px; text-align: left; font-size: 0.78rem; text-transform: uppercase; letter-spacing: .06em; color: #64748b; border-bottom: 1px solid #2d3250; }
tbody tr { border-bottom: 1px solid #1e2235; transition: background .1s; }
tbody tr:hover { background: #1a1d2e; }
td { padding: 11px 14px; font-size: 0.9rem; }
td.ip { font-family: monospace; color: #7c8cf8; font-weight: 600; }
td.mac { font-family: monospace; font-size: 0.82rem; color: #94a3b8; }
td.vendor { font-size: 0.82rem; color: #64748b; }
td.hostname { font-size: 0.82rem; color: #94a3b8; }
.name-cell { display: flex; gap: 8px; align-items: center; }
.name-input { background: #252840; border: 1px solid #3d4466; color: #e2e8f0; border-radius: 6px; padding: 5px 10px; font-size: 0.88rem; flex: 1; min-width: 120px; transition: border-color .15s; }
.name-input:focus { outline: none; border-color: #7c8cf8; }
.save-btn { background: #1e2235; color: #7c8cf8; border: 1px solid #3d4466; border-radius: 6px; padding: 5px 12px; font-size: 0.82rem; cursor: pointer; white-space: nowrap; transition: all .15s; }
.save-btn:hover { background: #2d3250; border-color: #7c8cf8; }
.saved-flash { color: #22c55e; font-size: 0.78rem; }
.empty { text-align: center; padding: 60px; color: #64748b; }
.badge { display: inline-block; background: #22c55e22; color: #22c55e; border-radius: 4px; padding: 2px 8px; font-size: 0.75rem; font-weight: 700; }
</style>
</head>
<body>
<header>
<h1>🌐 NetMap</h1>
<span>192.168.1.10 — 192.168.1.150</span>
</header>
<div class="container">
<div class="toolbar">
<button id="scanBtn" onclick="scan()">🔍 Scanner le réseau</button>
<span class="status" id="status">Cliquez sur Scanner pour démarrer</span>
</div>
<div id="result"></div>
</div>
<script>
let allDevices = {};
async function scan() {
const btn = document.getElementById('scanBtn');
const status = document.getElementById('status');
btn.disabled = true;
status.className = 'status scanning';
status.textContent = '⏳ Scan en cours (peut prendre 3060 s)…';
try {
const r = await fetch('/scan');
const data = await r.json();
status.className = 'status done';
if (data.error) {
status.textContent = '❌ Erreur : ' + data.error;
} else {
status.textContent = `✅ ${data.hosts.length} appareil(s) détecté(s)`;
allDevices = data.names || {};
renderTable(data.hosts, allDevices);
}
} catch(e) {
status.className = 'status';
status.textContent = '❌ Erreur réseau : ' + e;
}
btn.disabled = false;
}
function renderTable(hosts, names) {
const el = document.getElementById('result');
if (!hosts.length) { el.innerHTML = '<p class="empty">Aucun appareil détecté.</p>'; return; }
let rows = hosts.map(h => {
const name = names[h.ip] || '';
return `<tr>
<td class="ip">${h.ip}</td>
<td class="mac">${h.mac || ''}</td>
<td class="vendor">${h.vendor || ''}</td>
<td class="hostname">${h.hostname || ''}</td>
<td>
<div class="name-cell">
<input class="name-input" id="n_${h.ip.replace(/\./g,'_')}" value="${escHtml(name)}" placeholder="Nom personnalisé…">
<button class="save-btn" onclick="saveName('${h.ip}')">Sauver</button>
<span class="saved-flash" id="f_${h.ip.replace(/\./g,'_')}"></span>
</div>
</td>
</tr>`;
}).join('');
el.innerHTML = `<table>
<thead><tr>
<th>IP</th><th>MAC</th><th>Fabricant</th><th>Hostname</th><th>Nom perso</th>
</tr></thead>
<tbody>${rows}</tbody>
</table>`;
}
function escHtml(s) { return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
async function saveName(ip) {
const key = ip.replace(/\./g,'_');
const val = document.getElementById('n_'+key).value.trim();
const flash = document.getElementById('f_'+key);
try {
const r = await fetch('/name', {
method: 'POST',
headers: {'Content-Type':'application/json'},
body: JSON.stringify({ip, name: val})
});
const d = await r.json();
flash.textContent = d.ok ? '' : '';
setTimeout(() => flash.textContent = '', 2000);
} catch(e) { flash.textContent = ''; }
}
// Charger les noms sauvegardés au démarrage
fetch('/names').then(r => r.json()).then(d => { allDevices = d; });
</script>
</body>
</html>"""
class Handler(BaseHTTPRequestHandler):
def log_message(self, fmt, *args):
pass # Silence les logs HTTP
def send_json(self, data, code=200):
body = json.dumps(data).encode()
self.send_response(code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", len(body))
self.end_headers()
self.wfile.write(body)
def do_GET(self):
path = urlparse(self.path).path
if path == "/" or path == "/index.html":
body = HTML.encode()
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", len(body))
self.end_headers()
self.wfile.write(body)
elif path == "/scan":
hosts = run_scan()
if isinstance(hosts, dict) and "error" in hosts:
self.send_json({"error": hosts["error"], "hosts": [], "names": {}})
else:
names = load_devices()
self.send_json({"hosts": hosts, "names": names})
elif path == "/names":
self.send_json(load_devices())
else:
self.send_response(404)
self.end_headers()
def do_POST(self):
if self.path == "/name":
length = int(self.headers.get("Content-Length", 0))
body = json.loads(self.rfile.read(length))
ip = body.get("ip", "").strip()
name = body.get("name", "").strip()
if re.match(r"^\d+\.\d+\.\d+\.\d+$", ip):
data = load_devices()
if name:
data[ip] = name
elif ip in data:
del data[ip]
save_devices(data)
self.send_json({"ok": True})
else:
self.send_json({"ok": False, "error": "IP invalide"}, 400)
else:
self.send_response(404)
self.end_headers()
if __name__ == "__main__":
server = HTTPServer(("0.0.0.0", PORT), Handler)
print(f"NetMap démarré → http://localhost:{PORT}")
print("Ctrl+C pour arrêter")
try:
server.serve_forever()
except KeyboardInterrupt:
print("\nArrêt.")
Executable
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
cd "$(dirname "$0")"
echo "NetMap → http://localhost:8787"
sudo python3 netmap.py