Major feature enhancement: Windows PowerShell network scanning integration

- Added Windows PowerShell network scanner with auto-detection and interactive mode
- Implemented dual scanning system (Windows + Linux fallback)
- Added computer management features (rename, delete, duplicate checking)
- Enhanced UI with modern responsive design and Romanian localization
- Added comprehensive Windows-Linux integration with WSL interop
- Improved error handling and user feedback throughout
- Added hot reload for development and comprehensive documentation

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-09-05 17:27:27 +03:00
parent 616763c603
commit acf234c600
13 changed files with 2477 additions and 644 deletions

View File

@@ -95,58 +95,256 @@ class WOLManager:
if not re.match(mac_pattern, mac):
return {'success': False, 'message': 'MAC address invalid!'}
# Verifică duplicate - încarcă toate calculatoarele existente
computers = self.load_computers()
# Verifică dacă există deja un calculator cu același MAC
for computer in computers:
if computer['mac'].lower() == mac.lower():
return {'success': False, 'message': f'Un calculator cu MAC {mac} există deja: {computer["name"]}'}
# Verifică dacă există deja un calculator cu același nume
for computer in computers:
if computer['name'].lower() == name.lower():
return {'success': False, 'message': f'Un calculator cu numele "{name}" există deja'}
# Verifică dacă există deja un calculator cu același IP (dacă IP-ul este furnizat)
if ip and ip.strip():
for computer in computers:
if computer.get('ip') and computer['ip'].lower() == ip.lower():
return {'success': False, 'message': f'Un calculator cu IP {ip} există deja: {computer["name"]}'}
# Adaugă în fișier
with open(CONFIG_FILE, 'a') as f:
f.write(f"{name}|{mac}|{ip}\n")
return {'success': True, 'message': f'Calculator {name} adăugat!'}
def scan_network(self):
def rename_computer(self, old_name, new_name):
if not new_name.strip():
return {'success': False, 'message': 'Numele nou nu poate fi gol!'}
computers = []
found = False
# Citește toate computerele
if os.path.exists(CONFIG_FILE):
with open(CONFIG_FILE, 'r') as f:
for line in f:
line = line.strip()
if line and not line.startswith('#'):
parts = line.split('|')
if len(parts) >= 2:
if parts[0] == old_name:
# Redenumește computerul găsit
computers.append(f"{new_name}|{parts[1]}|{parts[2] if len(parts) > 2 else ''}")
found = True
else:
computers.append(line)
else:
computers.append(line)
if not found:
return {'success': False, 'message': f'Computerul {old_name} nu a fost găsit!'}
# Verifică dacă noul nume există deja
for computer_line in computers:
if not computer_line.startswith('#') and computer_line.strip():
parts = computer_line.split('|')
if len(parts) >= 2 and parts[0] == new_name and computer_line != f"{new_name}|{parts[1]}|{parts[2] if len(parts) > 2 else ''}":
return {'success': False, 'message': f'Numele {new_name} este deja folosit!'}
# Rescrie fișierul
with open(CONFIG_FILE, 'w') as f:
for computer_line in computers:
f.write(computer_line + '\n')
return {'success': True, 'message': f'Computerul a fost redenumit din {old_name} în {new_name}!'}
def delete_computer(self, name=None, mac=None):
if not name and not mac:
return {'success': False, 'message': 'Trebuie specificat numele sau MAC-ul computerului!'}
computers = []
found = False
deleted_name = None
# Citește toate computerele
if os.path.exists(CONFIG_FILE):
with open(CONFIG_FILE, 'r') as f:
for line in f:
line = line.strip()
if line and not line.startswith('#'):
parts = line.split('|')
if len(parts) >= 2:
# Verifică dacă este computerul de șters (prin nume sau MAC)
if (name and parts[0] == name) or (mac and parts[1].lower() == mac.lower()):
found = True
deleted_name = parts[0] if parts[0] else f"Calculator cu MAC {parts[1]}"
# Nu adaugă linia în lista, efectiv ștergând computerul
else:
computers.append(line)
else:
computers.append(line)
if not found:
identifier = name if name else f"MAC {mac}"
return {'success': False, 'message': f'Computerul cu {identifier} nu a fost găsit!'}
# Rescrie fișierul fără computerul șters
with open(CONFIG_FILE, 'w') as f:
for computer_line in computers:
f.write(computer_line + '\n')
return {'success': True, 'message': f'Computerul {deleted_name} a fost șters!'}
def scan_network(self, custom_network=None):
try:
# Detectează rețeaua locală
result = subprocess.run(['ip', 'route'], capture_output=True, text=True)
# Încearcă să citească rezultatele scanului Windows mai întâi
windows_scan_result = self.try_read_windows_scan_results(custom_network)
if windows_scan_result:
return windows_scan_result
# Fallback la scanarea Linux tradițională
return self.scan_network_linux(custom_network)
except Exception as e:
return {'success': False, 'message': f'Eroare la scanare: {str(e)}'}
def try_read_windows_scan_results(self, custom_network=None):
"""Încearcă să citească rezultatele din scanul Windows"""
try:
import json
from datetime import datetime, timedelta
results_file = '/data/network-scan-results.json'
# Verifică dacă există fișierul cu rezultate
if not os.path.exists(results_file):
return None
# Citește rezultatele (cu suport pentru UTF-8 BOM din PowerShell)
with open(results_file, 'r', encoding='utf-8-sig') as f:
data = json.load(f)
# Verifică vârsta rezultatelor (nu mai vechi de 30 minute)
try:
timestamp = datetime.fromisoformat(data.get('timestamp', '').replace('Z', '+00:00'))
if datetime.now().replace(tzinfo=timestamp.tzinfo) - timestamp > timedelta(minutes=30):
return None # Rezultatele sunt prea vechi
except:
return None # Timestamp invalid
# Filtrează rezultatele pe baza rețelei specificate
computers = data.get('computers', [])
if custom_network and computers:
import ipaddress
try:
net = ipaddress.ip_network(custom_network, strict=False)
filtered_computers = []
for computer in computers:
if ipaddress.ip_address(computer.get('ip', '')) in net:
filtered_computers.append(computer)
computers = filtered_computers
except:
pass # Rămân toate computerele dacă validarea eșuează
# Returnează rezultatul adaptat
if not computers and custom_network:
return {
'success': True,
'computers': [],
'message': f'Nu s-au găsit dispozitive în rețeaua {custom_network}. Rulează scanul Windows pentru rezultate actualizate.'
}
message = data.get('message', f'Scanare Windows: găsite {len(computers)} dispozitive')
if custom_network:
message += f' în rețeaua {custom_network}'
return {
'success': True,
'computers': computers,
'message': message
}
except Exception as e:
# Nu afișa eroarea, încearcă scanarea Linux
return None
def scan_network_linux(self, custom_network=None):
"""Scanarea tradițională Linux pentru compatibilitate"""
if custom_network:
# Validează formatul rețelei CIDR
import ipaddress
try:
ipaddress.ip_network(custom_network, strict=False)
network = custom_network
except ValueError:
return {'success': False, 'message': f'Format rețea invalid: {custom_network}. Folosește format CIDR (ex: 192.168.1.0/24)'}
else:
# Detectează rețeaua locală folosind route (net-tools)
result = subprocess.run(['route', '-n'], capture_output=True, text=True)
network = '192.168.1.0/24' # default
for line in result.stdout.split('\n'):
if 'default' in line:
if '0.0.0.0' in line and 'UG' in line: # default gateway
parts = line.split()
if len(parts) >= 3:
gateway = parts[2]
if len(parts) >= 2:
gateway = parts[1]
# Construiește rețeaua bazată pe gateway
network_parts = gateway.split('.')
network = f"{network_parts[0]}.{network_parts[1]}.{network_parts[2]}.0/24"
break
# Scanează rețeaua
subprocess.run(['nmap', '-sn', network], capture_output=True, timeout=30)
# Citește ARP table
result = subprocess.run(['arp', '-a'], capture_output=True, text=True)
scanned = []
for line in result.stdout.split('\n'):
# Regex pentru parsarea ARP
match = re.search(r'\((\d+\.\d+\.\d+\.\d+)\).*([0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2})', line)
if match:
ip = match.group(1)
mac = match.group(2)
hostname = line.split()[0] if line.split() else 'unknown'
# Încearcă să obțină MAC addresses din tabela ARP pentru dispozitive cunoscute
arp_result = subprocess.run(['arp', '-a'], capture_output=True, text=True)
scanned = []
for line in arp_result.stdout.split('\n'):
# Regex pentru parsarea ARP
match = re.search(r'\((\d+\.\d+\.\d+\.\d+)\).*([0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2})', line)
if match:
ip = match.group(1)
mac = match.group(2)
hostname = line.split()[0] if line.split() else '?'
# Verifică dacă IP-ul este în rețeaua specificată
if custom_network:
import ipaddress
try:
net = ipaddress.ip_network(custom_network, strict=False)
if ipaddress.ip_address(ip) in net:
scanned.append({
'ip': ip,
'mac': mac,
'hostname': hostname,
'status': self.ping_computer(ip)
})
except:
pass
else:
# Autodetectare - adaugă toate intrările ARP
scanned.append({
'ip': ip,
'mac': mac,
'hostname': hostname,
'status': self.ping_computer(ip)
})
return {'success': True, 'computers': scanned}
except Exception as e:
return {'success': False, 'message': f'Eroare la scanare: {str(e)}'}
# Dacă nu s-au găsit dispozitive și este o rețea specificată custom, returnează mesaj informativ
if custom_network and not scanned:
return {
'success': True,
'computers': [],
'message': f'Nu s-au găsit dispozitive cu MAC addresses în rețeaua {custom_network}. În Docker Desktop Windows, scanarea automată este limitată. Rulează scanul Windows sau folosește butonul " Adaugă Calculator" pentru a adăuga manual dispozitivele cu IP și MAC cunoscute.'
}
return {'success': True, 'computers': scanned}
wol_manager = WOLManager()
@app.route('/')
def index():
# Hot reload is working!
return render_template('index.html')
@app.route('/api/computers')
@@ -191,10 +389,101 @@ def add_computer():
)
return jsonify(result)
@app.route('/api/scan')
def scan_network():
result = wol_manager.scan_network()
@app.route('/api/rename', methods=['POST'])
def rename_computer():
data = request.get_json()
result = wol_manager.rename_computer(
data.get('old_name'),
data.get('new_name')
)
return jsonify(result)
@app.route('/api/delete', methods=['POST'])
def delete_computer():
data = request.get_json()
result = wol_manager.delete_computer(
name=data.get('name'),
mac=data.get('mac')
)
return jsonify(result)
@app.route('/api/scan', methods=['GET', 'POST'])
def scan_network():
network = None
if request.method == 'POST':
data = request.get_json()
network = data.get('network') if data else None
elif request.method == 'GET':
network = request.args.get('network')
result = wol_manager.scan_network(network)
return jsonify(result)
@app.route('/api/scan/windows', methods=['POST'])
def trigger_windows_scan():
"""Declanșează un scan Windows prin apelarea script-ului PowerShell"""
try:
data = request.get_json()
network = data.get('network') if data else None
# Încearcă să execute PowerShell prin WSL interop
script_path = '/scripts/windows-network-scan.ps1'
output_path = '/data/network-scan-results.json'
# Construiește comanda PowerShell prin WSL interop
cmd = ['/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe',
'-ExecutionPolicy', 'Bypass',
'-File', script_path,
'-OutputPath', output_path,
'-Verbose']
if network:
cmd.extend(['-Network', network])
# Încearcă să execute script-ul prin WSL interop
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
if result.returncode == 0:
# Scanul a fost executat cu succes, citește rezultatele
scan_result = wol_manager.scan_network(network)
scan_result['message'] = 'Scan Windows executat automat cu succes!'
return jsonify(scan_result)
else:
# Dacă execuția automată eșuează, oferă instrucțiuni manuale
return jsonify({
'success': False,
'message': f'Execuția automată a eșuat: {result.stderr[:200]}...',
'auto_execution_failed': True,
'instructions': 'Rulează manual din Windows unul dintre comenzile:',
'commands': [
f'scripts\\scan-network.bat{" -network " + network if network else ""}',
f'scripts\\run-scan.ps1{" -Network " + network if network else ""}',
f'powershell.exe -ExecutionPolicy Bypass -File scripts\\windows-network-scan.ps1 -OutputPath data\\network-scan-results.json{" -Network " + network if network else ""}'
]
})
except (subprocess.TimeoutExpired, FileNotFoundError, PermissionError) as e:
# Execuția automată nu este posibilă, oferă instrucțiuni manuale
return jsonify({
'success': False,
'message': f'Execuția automată nu este disponibilă: {str(e)[:100]}',
'auto_execution_failed': True,
'instructions': 'Rulează manual din Windows unul dintre comenzile:',
'commands': [
f'scripts\\scan-network.bat{" -network " + network if network else ""}',
f'scripts\\run-scan.ps1{" -Network " + network if network else ""}',
f'powershell.exe -ExecutionPolicy Bypass -File scripts\\windows-network-scan.ps1 -OutputPath data\\network-scan-results.json{" -Network " + network if network else ""}'
]
})
except Exception as e:
return jsonify({
'success': False,
'message': f'Eroare: {str(e)}',
'instructions': 'Rulează manual: scripts\\scan-network.bat'
})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=False)
# Enable debug mode for development with hot reload
app.run(host='0.0.0.0', port=5000, debug=True, use_reloader=True)