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:
7
teamleader_test/app/scanner/__init__.py
Normal file
7
teamleader_test/app/scanner/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""Network scanner module."""
|
||||
|
||||
from app.scanner.network_scanner import NetworkScanner
|
||||
from app.scanner.port_scanner import PortScanner
|
||||
from app.scanner.service_detector import ServiceDetector
|
||||
|
||||
__all__ = ['NetworkScanner', 'PortScanner', 'ServiceDetector']
|
||||
242
teamleader_test/app/scanner/network_scanner.py
Normal file
242
teamleader_test/app/scanner/network_scanner.py
Normal file
@@ -0,0 +1,242 @@
|
||||
"""Network scanner implementation for host discovery."""
|
||||
|
||||
import socket
|
||||
import ipaddress
|
||||
import asyncio
|
||||
from typing import List, Set, Optional, Callable
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import logging
|
||||
|
||||
from app.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NetworkScanner:
|
||||
"""Scanner for discovering active hosts on a network."""
|
||||
|
||||
# Common ports for host discovery
|
||||
DISCOVERY_PORTS = [21, 22, 23, 25, 80, 443, 445, 3389, 8080, 8443]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
timeout: int = None,
|
||||
max_workers: int = None,
|
||||
progress_callback: Optional[Callable[[str, float], None]] = None
|
||||
):
|
||||
"""
|
||||
Initialize network scanner.
|
||||
|
||||
Args:
|
||||
timeout: Socket connection timeout in seconds
|
||||
max_workers: Maximum number of concurrent workers
|
||||
progress_callback: Optional callback for progress updates
|
||||
"""
|
||||
self.timeout = timeout or settings.default_scan_timeout
|
||||
self.max_workers = max_workers or settings.max_concurrent_scans
|
||||
self.progress_callback = progress_callback
|
||||
|
||||
async def scan_network(self, network_range: str) -> List[str]:
|
||||
"""
|
||||
Scan a network range for active hosts.
|
||||
|
||||
Args:
|
||||
network_range: Network in CIDR notation (e.g., '192.168.1.0/24')
|
||||
|
||||
Returns:
|
||||
List of active IP addresses
|
||||
"""
|
||||
logger.info(f"Starting network scan of {network_range}")
|
||||
|
||||
try:
|
||||
network = ipaddress.ip_network(network_range, strict=False)
|
||||
|
||||
# Validate private network if restriction enabled
|
||||
if settings.scan_private_networks_only and not network.is_private:
|
||||
raise ValueError(f"Network {network_range} is not a private network")
|
||||
|
||||
# Generate list of hosts to scan
|
||||
hosts = [str(ip) for ip in network.hosts()]
|
||||
total_hosts = len(hosts)
|
||||
|
||||
if total_hosts == 0:
|
||||
# Single host network
|
||||
hosts = [str(network.network_address)]
|
||||
total_hosts = 1
|
||||
|
||||
logger.info(f"Scanning {total_hosts} hosts in {network_range}")
|
||||
|
||||
# Scan hosts concurrently
|
||||
active_hosts = await self._scan_hosts_async(hosts)
|
||||
|
||||
logger.info(f"Scan completed. Found {len(active_hosts)} active hosts")
|
||||
return active_hosts
|
||||
|
||||
except ValueError as e:
|
||||
logger.error(f"Invalid network range: {e}")
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error during network scan: {e}")
|
||||
raise
|
||||
|
||||
async def _scan_hosts_async(self, hosts: List[str]) -> List[str]:
|
||||
"""
|
||||
Scan multiple hosts asynchronously.
|
||||
|
||||
Args:
|
||||
hosts: List of IP addresses to scan
|
||||
|
||||
Returns:
|
||||
List of active hosts
|
||||
"""
|
||||
active_hosts: Set[str] = set()
|
||||
total = len(hosts)
|
||||
completed = 0
|
||||
|
||||
# Use ThreadPoolExecutor for socket operations
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
|
||||
futures = []
|
||||
|
||||
for host in hosts:
|
||||
future = loop.run_in_executor(executor, self._check_host, host)
|
||||
futures.append((host, future))
|
||||
|
||||
# Process results as they complete
|
||||
for host, future in futures:
|
||||
try:
|
||||
is_active = await future
|
||||
if is_active:
|
||||
active_hosts.add(host)
|
||||
logger.debug(f"Host {host} is active")
|
||||
except Exception as e:
|
||||
logger.debug(f"Error checking host {host}: {e}")
|
||||
finally:
|
||||
completed += 1
|
||||
if self.progress_callback:
|
||||
progress = completed / total
|
||||
self.progress_callback(host, progress)
|
||||
|
||||
return sorted(list(active_hosts), key=lambda ip: ipaddress.ip_address(ip))
|
||||
|
||||
def _check_host(self, ip: str) -> bool:
|
||||
"""
|
||||
Check if a host is active by attempting TCP connections.
|
||||
|
||||
Args:
|
||||
ip: IP address to check
|
||||
|
||||
Returns:
|
||||
True if host responds on any discovery port
|
||||
"""
|
||||
for port in self.DISCOVERY_PORTS:
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(self.timeout)
|
||||
result = sock.connect_ex((ip, port))
|
||||
sock.close()
|
||||
|
||||
if result == 0:
|
||||
return True
|
||||
except socket.error:
|
||||
continue
|
||||
except Exception as e:
|
||||
logger.debug(f"Error checking {ip}:{port}: {e}")
|
||||
continue
|
||||
|
||||
return False
|
||||
|
||||
def get_local_network_range(self) -> Optional[str]:
|
||||
"""
|
||||
Detect local network range.
|
||||
|
||||
Returns:
|
||||
Network range in CIDR notation or None
|
||||
"""
|
||||
try:
|
||||
import netifaces
|
||||
|
||||
# Get default gateway interface
|
||||
gateways = netifaces.gateways()
|
||||
if 'default' not in gateways or netifaces.AF_INET not in gateways['default']:
|
||||
return None
|
||||
|
||||
default_interface = gateways['default'][netifaces.AF_INET][1]
|
||||
|
||||
# Get interface addresses
|
||||
addrs = netifaces.ifaddresses(default_interface)
|
||||
if netifaces.AF_INET not in addrs:
|
||||
return None
|
||||
|
||||
# Get IP and netmask
|
||||
inet_info = addrs[netifaces.AF_INET][0]
|
||||
ip = inet_info.get('addr')
|
||||
netmask = inet_info.get('netmask')
|
||||
|
||||
if not ip or not netmask:
|
||||
return None
|
||||
|
||||
# Calculate network address
|
||||
network = ipaddress.ip_network(f"{ip}/{netmask}", strict=False)
|
||||
return str(network)
|
||||
|
||||
except ImportError:
|
||||
logger.warning("netifaces not available, cannot detect local network")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error detecting local network: {e}")
|
||||
return None
|
||||
|
||||
def resolve_hostname(self, ip: str) -> Optional[str]:
|
||||
"""
|
||||
Resolve IP address to hostname.
|
||||
|
||||
Args:
|
||||
ip: IP address
|
||||
|
||||
Returns:
|
||||
Hostname or None
|
||||
"""
|
||||
try:
|
||||
hostname = socket.gethostbyaddr(ip)[0]
|
||||
return hostname
|
||||
except socket.herror:
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.debug(f"Error resolving {ip}: {e}")
|
||||
return None
|
||||
|
||||
def get_mac_address(self, ip: str) -> Optional[str]:
|
||||
"""
|
||||
Get MAC address for an IP (requires ARP access).
|
||||
|
||||
Args:
|
||||
ip: IP address
|
||||
|
||||
Returns:
|
||||
MAC address or None
|
||||
"""
|
||||
try:
|
||||
# Try to get MAC from ARP cache
|
||||
import subprocess
|
||||
import re
|
||||
|
||||
# Platform-specific ARP command
|
||||
import platform
|
||||
if platform.system() == 'Windows':
|
||||
arp_output = subprocess.check_output(['arp', '-a', ip]).decode()
|
||||
mac_pattern = r'([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})'
|
||||
else:
|
||||
arp_output = subprocess.check_output(['arp', '-n', ip]).decode()
|
||||
mac_pattern = r'([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}'
|
||||
|
||||
match = re.search(mac_pattern, arp_output)
|
||||
if match:
|
||||
return match.group(0).upper()
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Error getting MAC for {ip}: {e}")
|
||||
return None
|
||||
260
teamleader_test/app/scanner/nmap_scanner.py
Normal file
260
teamleader_test/app/scanner/nmap_scanner.py
Normal file
@@ -0,0 +1,260 @@
|
||||
"""Nmap integration for advanced scanning capabilities."""
|
||||
|
||||
import logging
|
||||
from typing import Optional, Dict, Any, List
|
||||
import asyncio
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NmapScanner:
|
||||
"""Wrapper for python-nmap with safe execution."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize nmap scanner."""
|
||||
self.nmap_available = self._check_nmap_available()
|
||||
if not self.nmap_available:
|
||||
logger.warning("nmap is not available on this system")
|
||||
|
||||
def _check_nmap_available(self) -> bool:
|
||||
"""
|
||||
Check if nmap is available on the system.
|
||||
|
||||
Returns:
|
||||
True if nmap is available
|
||||
"""
|
||||
try:
|
||||
import nmap
|
||||
nm = nmap.PortScanner()
|
||||
nm.nmap_version()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.debug(f"nmap not available: {e}")
|
||||
return False
|
||||
|
||||
async def scan_host(
|
||||
self,
|
||||
host: str,
|
||||
arguments: str = '-sT -T4'
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Scan a host using nmap.
|
||||
|
||||
Args:
|
||||
host: IP address or hostname
|
||||
arguments: Nmap arguments (default: TCP connect scan, aggressive timing)
|
||||
|
||||
Returns:
|
||||
Scan results dictionary or None
|
||||
"""
|
||||
if not self.nmap_available:
|
||||
logger.warning("Attempted to use nmap but it's not available")
|
||||
return None
|
||||
|
||||
try:
|
||||
import nmap
|
||||
|
||||
# Run nmap scan in thread pool
|
||||
loop = asyncio.get_event_loop()
|
||||
result = await loop.run_in_executor(
|
||||
None,
|
||||
self._run_nmap_scan,
|
||||
host,
|
||||
arguments
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error running nmap scan on {host}: {e}")
|
||||
return None
|
||||
|
||||
def _run_nmap_scan(self, host: str, arguments: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Run nmap scan synchronously.
|
||||
|
||||
Args:
|
||||
host: Host to scan
|
||||
arguments: Nmap arguments
|
||||
|
||||
Returns:
|
||||
Scan results
|
||||
"""
|
||||
try:
|
||||
import nmap
|
||||
|
||||
nm = nmap.PortScanner()
|
||||
|
||||
# Sanitize host input
|
||||
if not self._validate_host(host):
|
||||
logger.error(f"Invalid host: {host}")
|
||||
return None
|
||||
|
||||
# Execute scan
|
||||
logger.info(f"Running nmap scan: nmap {arguments} {host}")
|
||||
nm.scan(hosts=host, arguments=arguments)
|
||||
|
||||
# Parse results
|
||||
if host not in nm.all_hosts():
|
||||
logger.debug(f"No results for {host}")
|
||||
return None
|
||||
|
||||
host_info = nm[host]
|
||||
|
||||
# Extract relevant information
|
||||
result = {
|
||||
'hostname': host_info.hostname(),
|
||||
'state': host_info.state(),
|
||||
'protocols': list(host_info.all_protocols()),
|
||||
'ports': []
|
||||
}
|
||||
|
||||
# Extract port information
|
||||
for proto in host_info.all_protocols():
|
||||
ports = host_info[proto].keys()
|
||||
for port in ports:
|
||||
port_info = host_info[proto][port]
|
||||
result['ports'].append({
|
||||
'port': port,
|
||||
'protocol': proto,
|
||||
'state': port_info['state'],
|
||||
'service_name': port_info.get('name'),
|
||||
'service_version': port_info.get('version'),
|
||||
'service_product': port_info.get('product'),
|
||||
'extrainfo': port_info.get('extrainfo')
|
||||
})
|
||||
|
||||
# OS detection if available
|
||||
if 'osmatch' in host_info:
|
||||
result['os_matches'] = [
|
||||
{
|
||||
'name': os['name'],
|
||||
'accuracy': os['accuracy']
|
||||
}
|
||||
for os in host_info['osmatch']
|
||||
]
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in _run_nmap_scan for {host}: {e}")
|
||||
return None
|
||||
|
||||
def _validate_host(self, host: str) -> bool:
|
||||
"""
|
||||
Validate host input to prevent command injection.
|
||||
|
||||
Args:
|
||||
host: Host string to validate
|
||||
|
||||
Returns:
|
||||
True if valid
|
||||
"""
|
||||
import ipaddress
|
||||
import re
|
||||
|
||||
# Try as IP address
|
||||
try:
|
||||
ipaddress.ip_address(host)
|
||||
return True
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Try as network range
|
||||
try:
|
||||
ipaddress.ip_network(host, strict=False)
|
||||
return True
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Try as hostname (alphanumeric, dots, hyphens only)
|
||||
if re.match(r'^[a-zA-Z0-9.-]+$', host):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get_scan_arguments(
|
||||
self,
|
||||
scan_type: str,
|
||||
service_detection: bool = True,
|
||||
os_detection: bool = False,
|
||||
port_range: Optional[str] = None
|
||||
) -> str:
|
||||
"""
|
||||
Generate nmap arguments based on scan configuration.
|
||||
|
||||
Args:
|
||||
scan_type: Type of scan ('quick', 'standard', 'deep')
|
||||
service_detection: Enable service/version detection
|
||||
os_detection: Enable OS detection (requires root)
|
||||
port_range: Custom port range (e.g., '1-1000' or '80,443,8080')
|
||||
|
||||
Returns:
|
||||
Nmap argument string
|
||||
"""
|
||||
args = []
|
||||
|
||||
# Use TCP connect scan (no root required)
|
||||
args.append('-sT')
|
||||
|
||||
# Port specification
|
||||
if port_range:
|
||||
args.append(f'-p {port_range}')
|
||||
elif scan_type == 'quick':
|
||||
args.append('--top-ports 100')
|
||||
elif scan_type == 'standard':
|
||||
args.append('--top-ports 1000')
|
||||
elif scan_type == 'deep':
|
||||
args.append('-p-') # All ports
|
||||
|
||||
# Only show open ports
|
||||
args.append('--open')
|
||||
|
||||
# Timing
|
||||
if scan_type == 'quick':
|
||||
args.append('-T5') # Insane
|
||||
elif scan_type == 'deep':
|
||||
args.append('-T3') # Normal
|
||||
else:
|
||||
args.append('-T4') # Aggressive
|
||||
|
||||
# Service detection
|
||||
if service_detection:
|
||||
args.append('-sV')
|
||||
|
||||
# OS detection (requires root)
|
||||
if os_detection:
|
||||
args.append('-O')
|
||||
logger.warning("OS detection requires root privileges")
|
||||
|
||||
return ' '.join(args)
|
||||
|
||||
async def scan_network_with_nmap(
|
||||
self,
|
||||
network: str,
|
||||
scan_type: str = 'quick'
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Scan entire network using nmap.
|
||||
|
||||
Args:
|
||||
network: Network in CIDR notation
|
||||
scan_type: Type of scan
|
||||
|
||||
Returns:
|
||||
List of host results
|
||||
"""
|
||||
if not self.nmap_available:
|
||||
return []
|
||||
|
||||
try:
|
||||
arguments = self.get_scan_arguments(scan_type)
|
||||
result = await self.scan_host(network, arguments)
|
||||
|
||||
if result:
|
||||
return [result]
|
||||
return []
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error scanning network {network}: {e}")
|
||||
return []
|
||||
213
teamleader_test/app/scanner/port_scanner.py
Normal file
213
teamleader_test/app/scanner/port_scanner.py
Normal file
@@ -0,0 +1,213 @@
|
||||
"""Port scanner implementation."""
|
||||
|
||||
import socket
|
||||
import asyncio
|
||||
from typing import List, Dict, Set, Optional, Callable
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import logging
|
||||
|
||||
from app.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PortScanner:
|
||||
"""Scanner for detecting open ports on hosts."""
|
||||
|
||||
# Predefined port ranges for different scan types
|
||||
PORT_RANGES = {
|
||||
'quick': [21, 22, 23, 25, 53, 80, 110, 143, 443, 445, 3306, 3389, 5432, 8080, 8443],
|
||||
'standard': list(range(1, 1001)),
|
||||
'deep': list(range(1, 65536)),
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
timeout: int = None,
|
||||
max_workers: int = None,
|
||||
progress_callback: Optional[Callable[[str, int, float], None]] = None
|
||||
):
|
||||
"""
|
||||
Initialize port scanner.
|
||||
|
||||
Args:
|
||||
timeout: Socket connection timeout in seconds
|
||||
max_workers: Maximum number of concurrent workers
|
||||
progress_callback: Optional callback for progress updates (host, port, progress)
|
||||
"""
|
||||
self.timeout = timeout or settings.default_scan_timeout
|
||||
self.max_workers = max_workers or settings.max_concurrent_scans
|
||||
self.progress_callback = progress_callback
|
||||
|
||||
async def scan_host_ports(
|
||||
self,
|
||||
host: str,
|
||||
scan_type: str = 'quick',
|
||||
custom_ports: Optional[List[int]] = None
|
||||
) -> List[Dict[str, any]]:
|
||||
"""
|
||||
Scan ports on a single host.
|
||||
|
||||
Args:
|
||||
host: IP address or hostname
|
||||
scan_type: Type of scan ('quick', 'standard', 'deep', or 'custom')
|
||||
custom_ports: Custom port list (required if scan_type is 'custom')
|
||||
|
||||
Returns:
|
||||
List of dictionaries with port information
|
||||
"""
|
||||
logger.info(f"Starting port scan on {host} (type: {scan_type})")
|
||||
|
||||
# Determine ports to scan
|
||||
if scan_type == 'custom' and custom_ports:
|
||||
ports = custom_ports
|
||||
elif scan_type in self.PORT_RANGES:
|
||||
ports = self.PORT_RANGES[scan_type]
|
||||
else:
|
||||
ports = self.PORT_RANGES['quick']
|
||||
|
||||
# Scan ports
|
||||
open_ports = await self._scan_ports_async(host, ports)
|
||||
|
||||
logger.info(f"Scan completed on {host}. Found {len(open_ports)} open ports")
|
||||
return open_ports
|
||||
|
||||
async def _scan_ports_async(self, host: str, ports: List[int]) -> List[Dict[str, any]]:
|
||||
"""
|
||||
Scan multiple ports asynchronously.
|
||||
|
||||
Args:
|
||||
host: Host to scan
|
||||
ports: List of ports to scan
|
||||
|
||||
Returns:
|
||||
List of open port information
|
||||
"""
|
||||
open_ports = []
|
||||
total = len(ports)
|
||||
completed = 0
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
|
||||
futures = []
|
||||
|
||||
for port in ports:
|
||||
future = loop.run_in_executor(executor, self._check_port, host, port)
|
||||
futures.append((port, future))
|
||||
|
||||
# Process results
|
||||
for port, future in futures:
|
||||
try:
|
||||
result = await future
|
||||
if result:
|
||||
open_ports.append(result)
|
||||
logger.debug(f"Found open port {port} on {host}")
|
||||
except Exception as e:
|
||||
logger.debug(f"Error checking port {port} on {host}: {e}")
|
||||
finally:
|
||||
completed += 1
|
||||
if self.progress_callback:
|
||||
progress = completed / total
|
||||
self.progress_callback(host, port, progress)
|
||||
|
||||
return sorted(open_ports, key=lambda x: x['port'])
|
||||
|
||||
def _check_port(self, host: str, port: int) -> Optional[Dict[str, any]]:
|
||||
"""
|
||||
Check if a port is open on a host.
|
||||
|
||||
Args:
|
||||
host: Host to check
|
||||
port: Port number
|
||||
|
||||
Returns:
|
||||
Dictionary with port info if open, None otherwise
|
||||
"""
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(self.timeout)
|
||||
result = sock.connect_ex((host, port))
|
||||
sock.close()
|
||||
|
||||
if result == 0:
|
||||
return {
|
||||
'port': port,
|
||||
'protocol': 'tcp',
|
||||
'state': 'open',
|
||||
'service_name': self._guess_service_name(port)
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
except socket.error as e:
|
||||
logger.debug(f"Socket error checking {host}:{port}: {e}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.debug(f"Error checking {host}:{port}: {e}")
|
||||
return None
|
||||
|
||||
def _guess_service_name(self, port: int) -> Optional[str]:
|
||||
"""
|
||||
Guess service name based on well-known ports.
|
||||
|
||||
Args:
|
||||
port: Port number
|
||||
|
||||
Returns:
|
||||
Service name or None
|
||||
"""
|
||||
common_services = {
|
||||
20: 'ftp-data',
|
||||
21: 'ftp',
|
||||
22: 'ssh',
|
||||
23: 'telnet',
|
||||
25: 'smtp',
|
||||
53: 'dns',
|
||||
80: 'http',
|
||||
110: 'pop3',
|
||||
143: 'imap',
|
||||
443: 'https',
|
||||
445: 'smb',
|
||||
3306: 'mysql',
|
||||
3389: 'rdp',
|
||||
5432: 'postgresql',
|
||||
5900: 'vnc',
|
||||
8080: 'http-alt',
|
||||
8443: 'https-alt',
|
||||
}
|
||||
|
||||
return common_services.get(port)
|
||||
|
||||
def parse_port_range(self, port_range: str) -> List[int]:
|
||||
"""
|
||||
Parse port range string to list of ports.
|
||||
|
||||
Args:
|
||||
port_range: String like "80,443,8000-8100"
|
||||
|
||||
Returns:
|
||||
List of port numbers
|
||||
"""
|
||||
ports = set()
|
||||
|
||||
try:
|
||||
for part in port_range.split(','):
|
||||
part = part.strip()
|
||||
|
||||
if '-' in part:
|
||||
# Range like "8000-8100"
|
||||
start, end = map(int, part.split('-'))
|
||||
if 1 <= start <= end <= 65535:
|
||||
ports.update(range(start, end + 1))
|
||||
else:
|
||||
# Single port
|
||||
port = int(part)
|
||||
if 1 <= port <= 65535:
|
||||
ports.add(port)
|
||||
|
||||
return sorted(list(ports))
|
||||
|
||||
except ValueError as e:
|
||||
logger.error(f"Error parsing port range '{port_range}': {e}")
|
||||
return []
|
||||
250
teamleader_test/app/scanner/service_detector.py
Normal file
250
teamleader_test/app/scanner/service_detector.py
Normal file
@@ -0,0 +1,250 @@
|
||||
"""Service detection and banner grabbing implementation."""
|
||||
|
||||
import socket
|
||||
import logging
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ServiceDetector:
|
||||
"""Detector for identifying services running on open ports."""
|
||||
|
||||
def __init__(self, timeout: int = 3):
|
||||
"""
|
||||
Initialize service detector.
|
||||
|
||||
Args:
|
||||
timeout: Socket timeout in seconds
|
||||
"""
|
||||
self.timeout = timeout
|
||||
|
||||
def detect_service(self, host: str, port: int) -> Dict[str, Any]:
|
||||
"""
|
||||
Detect service on a specific port.
|
||||
|
||||
Args:
|
||||
host: Host IP or hostname
|
||||
port: Port number
|
||||
|
||||
Returns:
|
||||
Dictionary with service information
|
||||
"""
|
||||
service_info = {
|
||||
'port': port,
|
||||
'protocol': 'tcp',
|
||||
'service_name': None,
|
||||
'service_version': None,
|
||||
'banner': None
|
||||
}
|
||||
|
||||
# Try banner grabbing
|
||||
banner = self.grab_banner(host, port)
|
||||
if banner:
|
||||
service_info['banner'] = banner
|
||||
|
||||
# Try to identify service from banner
|
||||
service_name, version = self._identify_from_banner(banner, port)
|
||||
if service_name:
|
||||
service_info['service_name'] = service_name
|
||||
if version:
|
||||
service_info['service_version'] = version
|
||||
|
||||
# If no banner, use port-based guess
|
||||
if not service_info['service_name']:
|
||||
service_info['service_name'] = self._guess_service_from_port(port)
|
||||
|
||||
return service_info
|
||||
|
||||
def grab_banner(self, host: str, port: int) -> Optional[str]:
|
||||
"""
|
||||
Attempt to grab service banner.
|
||||
|
||||
Args:
|
||||
host: Host IP or hostname
|
||||
port: Port number
|
||||
|
||||
Returns:
|
||||
Banner string or None
|
||||
"""
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(self.timeout)
|
||||
sock.connect((host, port))
|
||||
|
||||
# Try to receive banner
|
||||
try:
|
||||
banner = sock.recv(1024)
|
||||
banner_str = banner.decode('utf-8', errors='ignore').strip()
|
||||
sock.close()
|
||||
|
||||
if banner_str:
|
||||
logger.debug(f"Got banner from {host}:{port}: {banner_str[:100]}")
|
||||
return banner_str
|
||||
except socket.timeout:
|
||||
# Try sending a probe for services that need it
|
||||
banner_str = self._probe_service(sock, port)
|
||||
sock.close()
|
||||
return banner_str
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Error grabbing banner from {host}:{port}: {e}")
|
||||
|
||||
return None
|
||||
|
||||
def _probe_service(self, sock: socket.socket, port: int) -> Optional[str]:
|
||||
"""
|
||||
Send service-specific probe to elicit response.
|
||||
|
||||
Args:
|
||||
sock: Connected socket
|
||||
port: Port number
|
||||
|
||||
Returns:
|
||||
Response string or None
|
||||
"""
|
||||
probes = {
|
||||
80: b"GET / HTTP/1.0\r\n\r\n",
|
||||
443: b"GET / HTTP/1.0\r\n\r\n",
|
||||
8080: b"GET / HTTP/1.0\r\n\r\n",
|
||||
8443: b"GET / HTTP/1.0\r\n\r\n",
|
||||
25: b"EHLO test\r\n",
|
||||
110: b"USER test\r\n",
|
||||
143: b"A001 CAPABILITY\r\n",
|
||||
}
|
||||
|
||||
probe = probes.get(port, b"\r\n")
|
||||
|
||||
try:
|
||||
sock.send(probe)
|
||||
response = sock.recv(1024)
|
||||
return response.decode('utf-8', errors='ignore').strip()
|
||||
except:
|
||||
return None
|
||||
|
||||
def _identify_from_banner(self, banner: str, port: int) -> tuple[Optional[str], Optional[str]]:
|
||||
"""
|
||||
Identify service and version from banner.
|
||||
|
||||
Args:
|
||||
banner: Banner string
|
||||
port: Port number
|
||||
|
||||
Returns:
|
||||
Tuple of (service_name, version)
|
||||
"""
|
||||
banner_lower = banner.lower()
|
||||
|
||||
# HTTP servers
|
||||
if 'http' in banner_lower or port in [80, 443, 8080, 8443]:
|
||||
if 'apache' in banner_lower:
|
||||
return self._extract_apache_version(banner)
|
||||
elif 'nginx' in banner_lower:
|
||||
return self._extract_nginx_version(banner)
|
||||
elif 'iis' in banner_lower or 'microsoft' in banner_lower:
|
||||
return 'IIS', None
|
||||
else:
|
||||
return 'HTTP', None
|
||||
|
||||
# SSH
|
||||
if 'ssh' in banner_lower or port == 22:
|
||||
if 'openssh' in banner_lower:
|
||||
return self._extract_openssh_version(banner)
|
||||
return 'SSH', None
|
||||
|
||||
# FTP
|
||||
if 'ftp' in banner_lower or port in [20, 21]:
|
||||
if 'filezilla' in banner_lower:
|
||||
return 'FileZilla FTP', None
|
||||
elif 'proftpd' in banner_lower:
|
||||
return 'ProFTPD', None
|
||||
return 'FTP', None
|
||||
|
||||
# SMTP
|
||||
if 'smtp' in banner_lower or 'mail' in banner_lower or port == 25:
|
||||
if 'postfix' in banner_lower:
|
||||
return 'Postfix', None
|
||||
elif 'exim' in banner_lower:
|
||||
return 'Exim', None
|
||||
return 'SMTP', None
|
||||
|
||||
# MySQL
|
||||
if 'mysql' in banner_lower or port == 3306:
|
||||
return 'MySQL', None
|
||||
|
||||
# PostgreSQL
|
||||
if 'postgresql' in banner_lower or port == 5432:
|
||||
return 'PostgreSQL', None
|
||||
|
||||
# Generic identification
|
||||
if port == 22:
|
||||
return 'SSH', None
|
||||
elif port in [80, 8080]:
|
||||
return 'HTTP', None
|
||||
elif port in [443, 8443]:
|
||||
return 'HTTPS', None
|
||||
|
||||
return None, None
|
||||
|
||||
def _extract_apache_version(self, banner: str) -> tuple[str, Optional[str]]:
|
||||
"""Extract Apache version from banner."""
|
||||
import re
|
||||
match = re.search(r'Apache/?([\d.]+)?', banner, re.IGNORECASE)
|
||||
if match:
|
||||
version = match.group(1)
|
||||
return 'Apache', version
|
||||
return 'Apache', None
|
||||
|
||||
def _extract_nginx_version(self, banner: str) -> tuple[str, Optional[str]]:
|
||||
"""Extract nginx version from banner."""
|
||||
import re
|
||||
match = re.search(r'nginx/?([\d.]+)?', banner, re.IGNORECASE)
|
||||
if match:
|
||||
version = match.group(1)
|
||||
return 'nginx', version
|
||||
return 'nginx', None
|
||||
|
||||
def _extract_openssh_version(self, banner: str) -> tuple[str, Optional[str]]:
|
||||
"""Extract OpenSSH version from banner."""
|
||||
import re
|
||||
match = re.search(r'OpenSSH[_/]?([\d.]+\w*)?', banner, re.IGNORECASE)
|
||||
if match:
|
||||
version = match.group(1)
|
||||
return 'OpenSSH', version
|
||||
return 'OpenSSH', None
|
||||
|
||||
def _guess_service_from_port(self, port: int) -> Optional[str]:
|
||||
"""
|
||||
Guess service name from well-known port number.
|
||||
|
||||
Args:
|
||||
port: Port number
|
||||
|
||||
Returns:
|
||||
Service name or None
|
||||
"""
|
||||
common_services = {
|
||||
20: 'ftp-data',
|
||||
21: 'ftp',
|
||||
22: 'ssh',
|
||||
23: 'telnet',
|
||||
25: 'smtp',
|
||||
53: 'dns',
|
||||
80: 'http',
|
||||
110: 'pop3',
|
||||
143: 'imap',
|
||||
443: 'https',
|
||||
445: 'smb',
|
||||
993: 'imaps',
|
||||
995: 'pop3s',
|
||||
3306: 'mysql',
|
||||
3389: 'rdp',
|
||||
5432: 'postgresql',
|
||||
5900: 'vnc',
|
||||
6379: 'redis',
|
||||
8080: 'http-alt',
|
||||
8443: 'https-alt',
|
||||
27017: 'mongodb',
|
||||
}
|
||||
|
||||
return common_services.get(port)
|
||||
Reference in New Issue
Block a user