Initial commit: Werkzeuge-Sammlung
Enthält: - rdp_client.py: RDP Client mit GUI und Monitor-Auswahl - rdp.sh: Bash-basierter RDP Client - teamleader_test/: Network Scanner Fullstack-App - teamleader_test2/: Network Mapper CLI Subdirectories mit eigenem Repo wurden ausgeschlossen. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
6
teamleader_test2/network_mapper/__init__.py
Normal file
6
teamleader_test2/network_mapper/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""LAN topology scanning package."""
|
||||
__all__ = [
|
||||
"scanner",
|
||||
"main",
|
||||
"cli",
|
||||
]
|
||||
61
teamleader_test2/network_mapper/cli.py
Normal file
61
teamleader_test2/network_mapper/cli.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""Command line entrypoints for the LAN visualizer."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import typer
|
||||
|
||||
from network_mapper.scanner import NetworkScanner
|
||||
|
||||
app = typer.Typer(help="LAN discovery + visualization tooling")
|
||||
|
||||
|
||||
@app.command()
|
||||
def scan(
|
||||
cidr: str | None = typer.Option(
|
||||
None,
|
||||
help="CIDR range to probe (defaults to the primary IPv4 network).",
|
||||
),
|
||||
concurrency: int = typer.Option(32, help="Limit the number of simultaneous pings."),
|
||||
ssh_timeout: float = typer.Option(5.0, help="Seconds to wait for SSH probes."),
|
||||
ssh_user: str | None = typer.Option(
|
||||
None,
|
||||
envvar="SSH_USER",
|
||||
help="Username for SSH probes when key auth is available.",
|
||||
),
|
||||
ssh_key: Path | None = typer.Option(
|
||||
None,
|
||||
envvar="SSH_KEY_PATH",
|
||||
exists=True,
|
||||
file_okay=True,
|
||||
dir_okay=False,
|
||||
help="Private key path for passwordless SSH execution.",
|
||||
),
|
||||
output: Path | None = typer.Option(
|
||||
None,
|
||||
exists=False,
|
||||
file_okay=True,
|
||||
dir_okay=False,
|
||||
help="Write the scan result JSON to this file instead of stdout.",
|
||||
),
|
||||
):
|
||||
"""Run a single scan and emit JSON results."""
|
||||
scanner = NetworkScanner(ssh_user=ssh_user, ssh_key_path=str(ssh_key) if ssh_key else None)
|
||||
result = asyncio.run(scanner.scan(cidr=cidr, concurrency=concurrency, ssh_timeout=ssh_timeout))
|
||||
payload = result.to_dict()
|
||||
serialized = json.dumps(payload, indent=2)
|
||||
if output:
|
||||
output.write_text(serialized)
|
||||
typer.echo(f"Scan saved to {output}")
|
||||
else:
|
||||
typer.echo(serialized)
|
||||
|
||||
|
||||
@app.command()
|
||||
def serve(host: str = "0.0.0.0", port: int = 8000, reload: bool = False):
|
||||
"""Start the FastAPI server and front-end."""
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run("network_mapper.main:app", host=host, port=port, reload=reload)
|
||||
35
teamleader_test2/network_mapper/main.py
Normal file
35
teamleader_test2/network_mapper/main.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""FastAPI application that exposes the scan API and serves the front-end."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from network_mapper.scanner import NetworkScanner
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
FRONTEND_DIR = BASE_DIR / "frontend"
|
||||
|
||||
app = FastAPI(title="LAN Graph Explorer")
|
||||
|
||||
scanner = NetworkScanner(
|
||||
ssh_user=os.environ.get("SSH_USER"),
|
||||
ssh_key_path=os.environ.get("SSH_KEY_PATH"),
|
||||
)
|
||||
|
||||
app.mount("/static", StaticFiles(directory=FRONTEND_DIR), name="static")
|
||||
|
||||
|
||||
@app.get("/api/scan")
|
||||
async def api_scan(cidr: str | None = None, concurrency: int = 64, ssh_timeout: float = 5.0):
|
||||
"""Run a fresh scan and return the topology graph."""
|
||||
result = await scanner.scan(cidr=cidr, concurrency=concurrency, ssh_timeout=ssh_timeout)
|
||||
return result.to_dict()
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def serve_frontend():
|
||||
return FileResponse(FRONTEND_DIR / "index.html")
|
||||
291
teamleader_test2/network_mapper/scanner.py
Normal file
291
teamleader_test2/network_mapper/scanner.py
Normal file
@@ -0,0 +1,291 @@
|
||||
"""Network scanning helpers for the LAN graph tool."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import ipaddress
|
||||
import json
|
||||
import re
|
||||
import socket
|
||||
import subprocess
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Iterable, List, Optional, Set, Tuple
|
||||
|
||||
NEIGHBOR_PATTERN = re.compile(r"^(?P<ip>\d+\.\d+\.\d+\.\d+)")
|
||||
|
||||
@dataclass
|
||||
class HostNode:
|
||||
ip: str
|
||||
dns_name: Optional[str]
|
||||
reachable: bool
|
||||
last_seen: float
|
||||
via_ssh: bool = False
|
||||
comment: Optional[str] = None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"ip": self.ip,
|
||||
"dns_name": self.dns_name,
|
||||
"reachable": self.reachable,
|
||||
"last_seen": self.last_seen,
|
||||
"via_ssh": self.via_ssh,
|
||||
"comment": self.comment,
|
||||
}
|
||||
|
||||
@dataclass
|
||||
class ConnectionEdge:
|
||||
source: str
|
||||
target: str
|
||||
relation: str
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"source": self.source,
|
||||
"target": self.target,
|
||||
"relation": self.relation,
|
||||
}
|
||||
|
||||
@dataclass
|
||||
class ScanResult:
|
||||
cidr: str
|
||||
gateway: str
|
||||
nodes: List[HostNode]
|
||||
edges: List[ConnectionEdge]
|
||||
generated_at: float
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"cidr": self.cidr,
|
||||
"gateway": self.gateway,
|
||||
"generated_at": self.generated_at,
|
||||
"nodes": [node.to_dict() for node in self.nodes],
|
||||
"edges": [edge.to_dict() for edge in self.edges],
|
||||
}
|
||||
|
||||
@dataclass
|
||||
class _HostProbe:
|
||||
ip: str
|
||||
reachable: bool
|
||||
dns_name: Optional[str]
|
||||
neighbors: Set[str]
|
||||
via_ssh: bool
|
||||
|
||||
|
||||
def parse_neighbor_ips(raw_output: str) -> Set[str]:
|
||||
ips: Set[str] = set()
|
||||
for line in raw_output.splitlines():
|
||||
match = NEIGHBOR_PATTERN.match(line.strip())
|
||||
if match:
|
||||
ips.add(match.group("ip"))
|
||||
return ips
|
||||
|
||||
|
||||
class NetworkScanner:
|
||||
def __init__(self, ssh_user: Optional[str] = None, ssh_key_path: Optional[str] = None):
|
||||
self.ssh_user = ssh_user
|
||||
self.ssh_key_path = ssh_key_path
|
||||
self._ping_concurrency = 64
|
||||
self._ssh_concurrency = 10
|
||||
self._ssh_semaphore: Optional[asyncio.Semaphore] = None
|
||||
|
||||
async def scan(
|
||||
self,
|
||||
cidr: Optional[str] = None,
|
||||
concurrency: Optional[int] = None,
|
||||
ssh_timeout: float = 5.0,
|
||||
) -> ScanResult:
|
||||
if concurrency:
|
||||
self._ping_concurrency = concurrency
|
||||
network, gateway, local_ip = self._discover_network(cidr)
|
||||
host_ips = list(network.hosts())
|
||||
semaphore = asyncio.Semaphore(self._ping_concurrency)
|
||||
self._ssh_semaphore = asyncio.Semaphore(self._ssh_concurrency)
|
||||
|
||||
probes = []
|
||||
for host in host_ips:
|
||||
probes.append(self._probe_host(str(host), semaphore, ssh_timeout))
|
||||
probe_results = await asyncio.gather(*probes)
|
||||
|
||||
nodes: List[HostNode] = []
|
||||
node_map: dict[str, HostNode] = {}
|
||||
edges: List[ConnectionEdge] = []
|
||||
|
||||
timestamp = time.time()
|
||||
|
||||
# add gateway and scanner host nodes first
|
||||
gateway_node = HostNode(
|
||||
ip=gateway,
|
||||
dns_name=None,
|
||||
reachable=True,
|
||||
last_seen=timestamp,
|
||||
comment="default gateway",
|
||||
)
|
||||
node_map[gateway] = gateway_node
|
||||
|
||||
local_node = HostNode(
|
||||
ip=local_ip,
|
||||
dns_name=None,
|
||||
reachable=True,
|
||||
last_seen=timestamp,
|
||||
comment="this scanner",
|
||||
)
|
||||
node_map[local_ip] = local_node
|
||||
|
||||
final_edges: List[ConnectionEdge] = []
|
||||
seen_edges: Set[Tuple[str, str, str]] = set()
|
||||
|
||||
def append_edge(source: str, target: str, relation: str) -> None:
|
||||
key = (source, target, relation)
|
||||
if key in seen_edges:
|
||||
return
|
||||
seen_edges.add(key)
|
||||
final_edges.append(ConnectionEdge(source=source, target=target, relation=relation))
|
||||
|
||||
for probe in probe_results:
|
||||
if not probe.reachable:
|
||||
continue
|
||||
node = HostNode(
|
||||
ip=probe.ip,
|
||||
dns_name=probe.dns_name,
|
||||
reachable=True,
|
||||
last_seen=timestamp,
|
||||
via_ssh=probe.via_ssh,
|
||||
)
|
||||
node_map[probe.ip] = node
|
||||
if probe.ip != local_ip:
|
||||
append_edge(local_ip, probe.ip, "scan")
|
||||
if probe.ip != gateway:
|
||||
append_edge(gateway, probe.ip, "gateway")
|
||||
|
||||
for probe in probe_results:
|
||||
if not probe.reachable or not probe.neighbors:
|
||||
continue
|
||||
source = probe.ip
|
||||
for neighbor in probe.neighbors:
|
||||
if neighbor not in node_map:
|
||||
continue
|
||||
append_edge(source, neighbor, "neighbor")
|
||||
|
||||
result = ScanResult(
|
||||
cidr=str(network),
|
||||
gateway=gateway,
|
||||
nodes=list(node_map.values()),
|
||||
edges=final_edges,
|
||||
generated_at=timestamp,
|
||||
)
|
||||
return result
|
||||
|
||||
async def _probe_host(self, ip: str, semaphore: asyncio.Semaphore, ssh_timeout: float) -> _HostProbe:
|
||||
async with semaphore:
|
||||
alive = await self._ping(ip)
|
||||
name = await asyncio.to_thread(self._resolve_dns, ip) if alive else None
|
||||
neighbors: Set[str] = set()
|
||||
via_ssh = False
|
||||
if alive:
|
||||
neighbors, via_ssh = await self._collect_neighbors(ip, ssh_timeout)
|
||||
return _HostProbe(ip=ip, reachable=alive, dns_name=name, neighbors=neighbors, via_ssh=via_ssh)
|
||||
|
||||
async def _ping(self, ip: str) -> bool:
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
"ping",
|
||||
"-c",
|
||||
"1",
|
||||
"-W",
|
||||
"1",
|
||||
ip,
|
||||
stdout=asyncio.subprocess.DEVNULL,
|
||||
stderr=asyncio.subprocess.DEVNULL,
|
||||
)
|
||||
await process.communicate()
|
||||
return process.returncode == 0
|
||||
|
||||
def _resolve_dns(self, ip: str) -> Optional[str]:
|
||||
try:
|
||||
return socket.gethostbyaddr(ip)[0]
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
async def _collect_neighbors(self, ip: str, ssh_timeout: float) -> Tuple[Set[str], bool]:
|
||||
ssh_command = ["ssh", "-o", "BatchMode=yes", "-o", f"ConnectTimeout={ssh_timeout}"]
|
||||
if self.ssh_key_path:
|
||||
ssh_command.extend(["-i", str(self.ssh_key_path)])
|
||||
target = f"{self.ssh_user}@{ip}" if self.ssh_user else ip
|
||||
ssh_command.append(target)
|
||||
ssh_command.extend(["ip", "-4", "neigh", "show"])
|
||||
|
||||
if self._ssh_semaphore is None:
|
||||
self._ssh_semaphore = asyncio.Semaphore(self._ssh_concurrency)
|
||||
async with self._ssh_semaphore:
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*ssh_command,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.DEVNULL,
|
||||
)
|
||||
stdout, _ = await process.communicate()
|
||||
|
||||
if process.returncode != 0:
|
||||
return set(), False
|
||||
neighbors = parse_neighbor_ips(stdout.decode("utf-8", errors="ignore"))
|
||||
return neighbors, True
|
||||
|
||||
def _discover_network(self, cidr_override: Optional[str]) -> Tuple[ipaddress.IPv4Network, str, str]:
|
||||
if cidr_override is not None:
|
||||
network = ipaddress.IPv4Network(cidr_override, strict=False)
|
||||
else:
|
||||
network = self._without_override()
|
||||
gateway = self._default_gateway()
|
||||
local_ip = self._local_ip_from_network(network)
|
||||
return network, gateway, local_ip
|
||||
|
||||
def _without_override(self) -> ipaddress.IPv4Network:
|
||||
output = subprocess.run(
|
||||
["ip", "-o", "-4", "addr", "show", "scope", "global"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
for line in output.stdout.splitlines():
|
||||
parts = line.split()
|
||||
if len(parts) < 4:
|
||||
continue
|
||||
iface = parts[1]
|
||||
if iface == "lo":
|
||||
continue
|
||||
inet = parts[3]
|
||||
return ipaddress.IPv4Network(inet, strict=False)
|
||||
raise RuntimeError("Unable to determine local IPv4 network")
|
||||
|
||||
def _default_gateway(self) -> str:
|
||||
output = subprocess.run(
|
||||
["ip", "route", "show", "default"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
for line in output.stdout.splitlines():
|
||||
parts = line.split()
|
||||
if "via" in parts:
|
||||
via_index = parts.index("via")
|
||||
return parts[via_index + 1]
|
||||
raise RuntimeError("Unable to determine default gateway")
|
||||
|
||||
def _local_ip_from_network(self, network: ipaddress.IPv4Network) -> str:
|
||||
output = subprocess.run(
|
||||
["ip", "-o", "-4", "addr", "show", "scope", "global"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
for line in output.stdout.splitlines():
|
||||
parts = line.split()
|
||||
if len(parts) < 4:
|
||||
continue
|
||||
iface = parts[1]
|
||||
if iface == "lo":
|
||||
continue
|
||||
inet = parts[3]
|
||||
if ipaddress.IPv4Address(inet.split("/")[0]) in network:
|
||||
return inet.split("/")[0]
|
||||
raise RuntimeError("Unable to determine scanner IP within network")
|
||||
Reference in New Issue
Block a user