Files
percoandClaude Sonnet 4.6 b70c4c923b 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>
2026-06-28 11:53:51 +02:00

263 lines
9.7 KiB
Python
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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.")