move
This commit is contained in:
@@ -33,6 +33,7 @@ class ScanConfig(BaseModel):
|
|||||||
class PortsConfig(BaseModel):
|
class PortsConfig(BaseModel):
|
||||||
"""Configuration des ports à scanner"""
|
"""Configuration des ports à scanner"""
|
||||||
ranges: List[str] = ["22", "80", "443", "3389", "8080"]
|
ranges: List[str] = ["22", "80", "443", "3389", "8080"]
|
||||||
|
protocols: Optional[Dict[int, str]] = None # Mapping port -> protocole
|
||||||
|
|
||||||
|
|
||||||
class HistoryConfig(BaseModel):
|
class HistoryConfig(BaseModel):
|
||||||
@@ -45,6 +46,11 @@ class UIConfig(BaseModel):
|
|||||||
offline_transparency: float = 0.5
|
offline_transparency: float = 0.5
|
||||||
show_mac: bool = True
|
show_mac: bool = True
|
||||||
show_vendor: bool = True
|
show_vendor: bool = True
|
||||||
|
cell_size: int = 30
|
||||||
|
font_size: int = 10
|
||||||
|
cell_gap: float = 2
|
||||||
|
details_font_size: int = 13
|
||||||
|
details_spacing: int = 2
|
||||||
|
|
||||||
|
|
||||||
class ColorsConfig(BaseModel):
|
class ColorsConfig(BaseModel):
|
||||||
@@ -61,15 +67,23 @@ class DatabaseConfig(BaseModel):
|
|||||||
path: str = "./data/db.sqlite"
|
path: str = "./data/db.sqlite"
|
||||||
|
|
||||||
|
|
||||||
|
class HostConfig(BaseModel):
|
||||||
|
"""Configuration d'un hôte avec sa localisation"""
|
||||||
|
name: str
|
||||||
|
location: str
|
||||||
|
|
||||||
|
|
||||||
class IPWatchConfig(BaseModel):
|
class IPWatchConfig(BaseModel):
|
||||||
"""Configuration complète IPWatch"""
|
"""Configuration complète IPWatch"""
|
||||||
|
model_config = {"arbitrary_types_allowed": True}
|
||||||
|
|
||||||
app: AppConfig = Field(default_factory=AppConfig)
|
app: AppConfig = Field(default_factory=AppConfig)
|
||||||
network: NetworkConfig
|
network: NetworkConfig
|
||||||
ip_classes: Dict[str, Any] = Field(default_factory=dict)
|
ip_classes: Dict[str, Any] = Field(default_factory=dict)
|
||||||
scan: ScanConfig = Field(default_factory=ScanConfig)
|
scan: ScanConfig = Field(default_factory=ScanConfig)
|
||||||
ports: PortsConfig = Field(default_factory=PortsConfig)
|
ports: PortsConfig = Field(default_factory=PortsConfig)
|
||||||
locations: List[str] = Field(default_factory=list)
|
locations: List[str] = Field(default_factory=list)
|
||||||
hosts: List[str] = Field(default_factory=list)
|
hosts: List[HostConfig] = Field(default_factory=list)
|
||||||
history: HistoryConfig = Field(default_factory=HistoryConfig)
|
history: HistoryConfig = Field(default_factory=HistoryConfig)
|
||||||
ui: UIConfig = Field(default_factory=UIConfig)
|
ui: UIConfig = Field(default_factory=UIConfig)
|
||||||
colors: ColorsConfig = Field(default_factory=ColorsConfig)
|
colors: ColorsConfig = Field(default_factory=ColorsConfig)
|
||||||
@@ -97,8 +111,15 @@ class ConfigManager:
|
|||||||
yaml_data = yaml.safe_load(f)
|
yaml_data = yaml.safe_load(f)
|
||||||
|
|
||||||
self._config = IPWatchConfig(**yaml_data)
|
self._config = IPWatchConfig(**yaml_data)
|
||||||
|
self._config_path = config_path
|
||||||
return self._config
|
return self._config
|
||||||
|
|
||||||
|
def reload_config(self) -> IPWatchConfig:
|
||||||
|
"""Recharge la configuration depuis le fichier"""
|
||||||
|
if not hasattr(self, '_config_path'):
|
||||||
|
self._config_path = "./config.yaml"
|
||||||
|
return self.load_config(self._config_path)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def config(self) -> IPWatchConfig:
|
def config(self) -> IPWatchConfig:
|
||||||
"""Retourne la configuration actuelle"""
|
"""Retourne la configuration actuelle"""
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from pathlib import Path
|
|||||||
from backend.app.core.config import config_manager
|
from backend.app.core.config import config_manager
|
||||||
from backend.app.core.database import init_database, get_db
|
from backend.app.core.database import init_database, get_db
|
||||||
from backend.app.routers import ips_router, scan_router, websocket_router
|
from backend.app.routers import ips_router, scan_router, websocket_router
|
||||||
|
from backend.app.routers import config as config_router
|
||||||
from backend.app.services.scheduler import scan_scheduler
|
from backend.app.services.scheduler import scan_scheduler
|
||||||
from backend.app.routers.scan import perform_scan
|
from backend.app.routers.scan import perform_scan
|
||||||
|
|
||||||
@@ -119,6 +120,7 @@ app.add_middleware(
|
|||||||
app.include_router(ips_router)
|
app.include_router(ips_router)
|
||||||
app.include_router(scan_router)
|
app.include_router(scan_router)
|
||||||
app.include_router(websocket_router)
|
app.include_router(websocket_router)
|
||||||
|
app.include_router(config_router.router)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ class IP(Base):
|
|||||||
mac = Column(String, nullable=True) # Adresse MAC
|
mac = Column(String, nullable=True) # Adresse MAC
|
||||||
vendor = Column(String, nullable=True) # Fabricant (lookup MAC)
|
vendor = Column(String, nullable=True) # Fabricant (lookup MAC)
|
||||||
hostname = Column(String, nullable=True) # Nom d'hôte réseau
|
hostname = Column(String, nullable=True) # Nom d'hôte réseau
|
||||||
|
link = Column(String, nullable=True) # Lien personnalisé (URL)
|
||||||
|
|
||||||
# Ports ouverts (stocké en JSON)
|
# Ports ouverts (stocké en JSON)
|
||||||
open_ports = Column(JSON, default=list) # Liste des ports ouverts
|
open_ports = Column(JSON, default=list) # Liste des ports ouverts
|
||||||
|
|||||||
40
backend/app/routers/config.py
Normal file
40
backend/app/routers/config.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
"""
|
||||||
|
Routes pour la configuration
|
||||||
|
"""
|
||||||
|
from fastapi import APIRouter, HTTPException
|
||||||
|
from backend.app.core.config import config_manager
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/config", tags=["config"])
|
||||||
|
|
||||||
|
@router.get("/ui")
|
||||||
|
async def get_ui_config():
|
||||||
|
"""Récupérer la configuration UI"""
|
||||||
|
config = config_manager.config
|
||||||
|
return {
|
||||||
|
"cell_size": config.ui.cell_size,
|
||||||
|
"font_size": config.ui.font_size,
|
||||||
|
"cell_gap": config.ui.cell_gap,
|
||||||
|
"offline_transparency": config.ui.offline_transparency,
|
||||||
|
"show_mac": config.ui.show_mac,
|
||||||
|
"show_vendor": config.ui.show_vendor
|
||||||
|
}
|
||||||
|
|
||||||
|
@router.post("/reload")
|
||||||
|
async def reload_config():
|
||||||
|
"""Recharger la configuration depuis le fichier config.yaml"""
|
||||||
|
try:
|
||||||
|
config = config_manager.reload_config()
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "Configuration rechargée avec succès",
|
||||||
|
"ui": {
|
||||||
|
"cell_size": config.ui.cell_size,
|
||||||
|
"font_size": config.ui.font_size,
|
||||||
|
"cell_gap": config.ui.cell_gap,
|
||||||
|
"offline_transparency": config.ui.offline_transparency,
|
||||||
|
"show_mac": config.ui.show_mac,
|
||||||
|
"show_vendor": config.ui.show_vendor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Erreur rechargement config: {str(e)}")
|
||||||
@@ -1,14 +1,17 @@
|
|||||||
"""
|
"""
|
||||||
Endpoints API pour la gestion des IPs
|
Endpoints API pour la gestion des IPs
|
||||||
"""
|
"""
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from sqlalchemy import desc
|
from sqlalchemy import desc
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
import yaml
|
||||||
|
|
||||||
from backend.app.core.database import get_db
|
from backend.app.core.database import get_db
|
||||||
from backend.app.models.ip import IP, IPHistory
|
from backend.app.models.ip import IP, IPHistory
|
||||||
|
from backend.app.core.config import config_manager
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/ips", tags=["IPs"])
|
router = APIRouter(prefix="/api/ips", tags=["IPs"])
|
||||||
@@ -21,6 +24,7 @@ class IPUpdate(BaseModel):
|
|||||||
known: Optional[bool] = None
|
known: Optional[bool] = None
|
||||||
location: Optional[str] = None
|
location: Optional[str] = None
|
||||||
host: Optional[str] = None
|
host: Optional[str] = None
|
||||||
|
link: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class IPResponse(BaseModel):
|
class IPResponse(BaseModel):
|
||||||
@@ -36,6 +40,7 @@ class IPResponse(BaseModel):
|
|||||||
mac: Optional[str]
|
mac: Optional[str]
|
||||||
vendor: Optional[str]
|
vendor: Optional[str]
|
||||||
hostname: Optional[str]
|
hostname: Optional[str]
|
||||||
|
link: Optional[str]
|
||||||
open_ports: List[int]
|
open_ports: List[int]
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
@@ -214,3 +219,189 @@ async def get_stats(db: Session = Depends(get_db)):
|
|||||||
"known": known,
|
"known": known,
|
||||||
"unknown": unknown
|
"unknown": unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/config/options")
|
||||||
|
async def get_config_options():
|
||||||
|
"""
|
||||||
|
Récupère les options de configuration (locations, hosts, port_protocols, version)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionnaire avec locations, hosts, port_protocols et version
|
||||||
|
"""
|
||||||
|
config = config_manager.config
|
||||||
|
|
||||||
|
# Récupérer les protocoles de ports depuis la config
|
||||||
|
port_protocols = {}
|
||||||
|
if hasattr(config.ports, 'protocols') and config.ports.protocols:
|
||||||
|
port_protocols = config.ports.protocols
|
||||||
|
|
||||||
|
return {
|
||||||
|
"locations": config.locations,
|
||||||
|
"hosts": [{"name": h.name, "location": h.location} for h in config.hosts],
|
||||||
|
"port_protocols": port_protocols,
|
||||||
|
"version": config.app.version
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/config/content")
|
||||||
|
async def get_config_content():
|
||||||
|
"""
|
||||||
|
Récupère le contenu brut du fichier config.yaml
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Contenu du fichier YAML
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open("./config.yaml", "r", encoding="utf-8") as f:
|
||||||
|
content = f.read()
|
||||||
|
return {"content": content}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Erreur lecture config: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/config/reload")
|
||||||
|
async def reload_config():
|
||||||
|
"""
|
||||||
|
Recharge la configuration depuis config.yaml
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Message de confirmation
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
config_manager.reload_config()
|
||||||
|
return {"message": "Configuration rechargée avec succès"}
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Erreur rechargement config: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/import/ipscan")
|
||||||
|
async def import_ipscan(file: UploadFile = File(...), db: Session = Depends(get_db)):
|
||||||
|
"""
|
||||||
|
Importe les données depuis un fichier XML Angry IP Scanner
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file: Fichier XML uploadé
|
||||||
|
db: Session de base de données
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Statistiques d'import
|
||||||
|
"""
|
||||||
|
if not file.filename.endswith('.xml'):
|
||||||
|
raise HTTPException(status_code=400, detail="Le fichier doit être un XML")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Lire le contenu du fichier
|
||||||
|
content = await file.read()
|
||||||
|
|
||||||
|
# Essayer de parser le XML avec récupération d'erreurs
|
||||||
|
try:
|
||||||
|
root = ET.fromstring(content)
|
||||||
|
except ET.ParseError as e:
|
||||||
|
# Si le parsing échoue, essayer de nettoyer le contenu
|
||||||
|
import re
|
||||||
|
content_str = content.decode('utf-8', errors='ignore')
|
||||||
|
|
||||||
|
# Supprimer les caractères de contrôle invalides (sauf tab, CR, LF)
|
||||||
|
content_str = ''.join(char for char in content_str
|
||||||
|
if ord(char) >= 32 or char in '\t\r\n')
|
||||||
|
|
||||||
|
try:
|
||||||
|
root = ET.fromstring(content_str.encode('utf-8'))
|
||||||
|
except ET.ParseError:
|
||||||
|
raise HTTPException(status_code=400, detail=f"Fichier XML invalide même après nettoyage: {str(e)}")
|
||||||
|
|
||||||
|
imported = 0
|
||||||
|
updated = 0
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
# Parser chaque host
|
||||||
|
for host in root.findall('.//host'):
|
||||||
|
try:
|
||||||
|
# Extraire l'adresse IP
|
||||||
|
ip_address = host.get('address')
|
||||||
|
if not ip_address:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Extraire les informations
|
||||||
|
hostname = None
|
||||||
|
mac = None
|
||||||
|
vendor = None
|
||||||
|
ports = []
|
||||||
|
|
||||||
|
for result in host.findall('result'):
|
||||||
|
name = result.get('name')
|
||||||
|
value = result.text.strip() if result.text else ""
|
||||||
|
|
||||||
|
# Nettoyer les valeurs [n/a]
|
||||||
|
if value == "[n/a]":
|
||||||
|
value = None
|
||||||
|
|
||||||
|
if name == "Nom d'hôte" and value:
|
||||||
|
hostname = value
|
||||||
|
elif name == "Adresse MAC" and value:
|
||||||
|
mac = value
|
||||||
|
elif name == "Constructeur MAC" and value:
|
||||||
|
vendor = value
|
||||||
|
elif name == "Ports" and value:
|
||||||
|
# Parser les ports (format: "22,80,443")
|
||||||
|
try:
|
||||||
|
ports = [int(p.strip()) for p in value.split(',') if p.strip().isdigit()]
|
||||||
|
except Exception as e:
|
||||||
|
ports = []
|
||||||
|
|
||||||
|
# Vérifier si l'IP existe déjà
|
||||||
|
existing_ip = db.query(IP).filter(IP.ip == ip_address).first()
|
||||||
|
|
||||||
|
if existing_ip:
|
||||||
|
# Mettre à jour avec de nouvelles informations
|
||||||
|
if hostname:
|
||||||
|
if not existing_ip.hostname:
|
||||||
|
existing_ip.hostname = hostname
|
||||||
|
if not existing_ip.name:
|
||||||
|
existing_ip.name = hostname
|
||||||
|
if mac and not existing_ip.mac:
|
||||||
|
existing_ip.mac = mac
|
||||||
|
# Toujours mettre à jour vendor et ports depuis IPScan (plus complet et à jour)
|
||||||
|
if vendor:
|
||||||
|
existing_ip.vendor = vendor
|
||||||
|
if ports:
|
||||||
|
existing_ip.open_ports = ports
|
||||||
|
existing_ip.last_status = "online"
|
||||||
|
existing_ip.last_seen = datetime.utcnow()
|
||||||
|
updated += 1
|
||||||
|
else:
|
||||||
|
# Créer une nouvelle entrée
|
||||||
|
new_ip = IP(
|
||||||
|
ip=ip_address,
|
||||||
|
name=hostname,
|
||||||
|
hostname=hostname,
|
||||||
|
mac=mac,
|
||||||
|
vendor=vendor,
|
||||||
|
open_ports=ports or [],
|
||||||
|
last_status="online",
|
||||||
|
known=False,
|
||||||
|
first_seen=datetime.utcnow(),
|
||||||
|
last_seen=datetime.utcnow()
|
||||||
|
)
|
||||||
|
db.add(new_ip)
|
||||||
|
imported += 1
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
errors.append(f"Erreur pour {ip_address}: {str(e)}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Commit des changements
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"message": "Import terminé",
|
||||||
|
"imported": imported,
|
||||||
|
"updated": updated,
|
||||||
|
"errors": errors[:10] # Limiter à 10 erreurs
|
||||||
|
}
|
||||||
|
|
||||||
|
except ET.ParseError as e:
|
||||||
|
raise HTTPException(status_code=400, detail=f"Fichier XML invalide: {str(e)}")
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Erreur import: {str(e)}")
|
||||||
|
|||||||
@@ -174,6 +174,72 @@ async def start_scan(background_tasks: BackgroundTasks, db: Session = Depends(ge
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/ports/{ip_address}")
|
||||||
|
async def scan_ip_ports(ip_address: str, db: Session = Depends(get_db)):
|
||||||
|
"""
|
||||||
|
Scanne les ports d'une IP spécifique
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ip_address: Adresse IP à scanner
|
||||||
|
db: Session de base de données
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Liste des ports ouverts
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Récupérer la config
|
||||||
|
config = config_manager.config
|
||||||
|
|
||||||
|
# Convertir les ports en liste d'entiers
|
||||||
|
port_list = []
|
||||||
|
for port_range in config.ports.ranges:
|
||||||
|
if '-' in port_range:
|
||||||
|
start, end = map(int, port_range.split('-'))
|
||||||
|
port_list.extend(range(start, end + 1))
|
||||||
|
else:
|
||||||
|
port_list.append(int(port_range))
|
||||||
|
|
||||||
|
# Initialiser le scanner
|
||||||
|
scanner = NetworkScanner(
|
||||||
|
cidr=config.network.cidr,
|
||||||
|
timeout=config.scan.timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
# Scanner les ports de cette IP
|
||||||
|
print(f"[{datetime.now()}] Scan ports pour {ip_address}...")
|
||||||
|
open_ports = await scanner.scan_ports(ip_address, port_list)
|
||||||
|
print(f"[{datetime.now()}] Ports ouverts pour {ip_address}: {open_ports}")
|
||||||
|
|
||||||
|
# Mettre à jour la base de données
|
||||||
|
ip_record = db.query(IP).filter(IP.ip == ip_address).first()
|
||||||
|
if ip_record:
|
||||||
|
ip_record.open_ports = open_ports
|
||||||
|
ip_record.last_seen = datetime.utcnow()
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# Notifier via WebSocket
|
||||||
|
await ws_manager.broadcast_ip_update({
|
||||||
|
"ip": ip_address,
|
||||||
|
"open_ports": open_ports
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"message": "Scan de ports terminé",
|
||||||
|
"ip": ip_address,
|
||||||
|
"open_ports": open_ports,
|
||||||
|
"timestamp": datetime.utcnow()
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Erreur scan ports {ip_address}: {e}")
|
||||||
|
return {
|
||||||
|
"message": f"Erreur: {str(e)}",
|
||||||
|
"ip": ip_address,
|
||||||
|
"open_ports": [],
|
||||||
|
"timestamp": datetime.utcnow()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/cleanup-history")
|
@router.post("/cleanup-history")
|
||||||
async def cleanup_history(hours: int = 24, db: Session = Depends(get_db)):
|
async def cleanup_history(hours: int = 24, db: Session = Depends(get_db)):
|
||||||
"""
|
"""
|
||||||
|
|||||||
64
config.yaml
64
config.yaml
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
app:
|
app:
|
||||||
name: "IPWatch"
|
name: "IPWatch"
|
||||||
version: "1.0.0"
|
version: "1.0.5"
|
||||||
debug: true
|
debug: true
|
||||||
|
|
||||||
network:
|
network:
|
||||||
@@ -23,7 +23,7 @@ subnets:
|
|||||||
- name: "dhcp"
|
- name: "dhcp"
|
||||||
cidr: "10.0.1.0/24"
|
cidr: "10.0.1.0/24"
|
||||||
start: "10.0.1.1"
|
start: "10.0.1.1"
|
||||||
end: "10.0.1locations.255"
|
end: "10.0.1.255"
|
||||||
description: "DHCP"
|
description: "DHCP"
|
||||||
- name: "iot"
|
- name: "iot"
|
||||||
cidr: "10.0.2.0/24"
|
cidr: "10.0.2.0/24"
|
||||||
@@ -49,25 +49,66 @@ ports:
|
|||||||
- "22" # SSH
|
- "22" # SSH
|
||||||
- "80" # HTTP
|
- "80" # HTTP
|
||||||
- "443" # HTTPS
|
- "443" # HTTPS
|
||||||
|
- "445" # SAMBA
|
||||||
|
- "1880" #nodered
|
||||||
|
- "3000"
|
||||||
- "3389" # RDP
|
- "3389" # RDP
|
||||||
- "8080" # HTTP alternatif
|
- "8080" # HTTP alternatif
|
||||||
|
- "8081" # HTTP alternatif
|
||||||
- "3306" # MySQL
|
- "3306" # MySQL
|
||||||
|
- "3552" #arcane
|
||||||
- "5432" # PostgreSQL
|
- "5432" # PostgreSQL
|
||||||
|
- "8006"
|
||||||
|
- "6053"
|
||||||
|
- "8266"
|
||||||
|
- "9000"
|
||||||
|
|
||||||
|
# Mapping port -> protocole pour générer des liens cliquables
|
||||||
|
protocols:
|
||||||
|
22: "ssh"
|
||||||
|
80: "http"
|
||||||
|
443: "https"
|
||||||
|
445: "smb"
|
||||||
|
1880: "http"
|
||||||
|
3000: "http"
|
||||||
|
3306: "mysql"
|
||||||
|
3389: "rdp"
|
||||||
|
3552: "http"
|
||||||
|
5432: "postgresql"
|
||||||
|
6053: "http"
|
||||||
|
8006: "https"
|
||||||
|
8080: "http"
|
||||||
|
8081: "http"
|
||||||
|
8266: "http"
|
||||||
|
9000: "http"
|
||||||
|
|
||||||
locations:
|
locations:
|
||||||
- "Bureau"
|
- "Bureau"
|
||||||
- "Salon"
|
- "Salon"
|
||||||
- "Comble"
|
- "Comble"
|
||||||
- "Bureau RdC"
|
- "Bureau RdC"
|
||||||
|
- "Garage"
|
||||||
|
- "Exterieur"
|
||||||
|
- "SdB"
|
||||||
|
- ""
|
||||||
|
|
||||||
# la localisation est herité de l'host il faudrait adapter config en consequence
|
# Hosts avec localisation associée
|
||||||
hosts:
|
hosts:
|
||||||
- "physique"
|
- name: "physique"
|
||||||
- "elitedesk"
|
location: ""
|
||||||
- "m710Q"
|
- name: "elitedesk"
|
||||||
- "HP Proliant"
|
location: "Comble"
|
||||||
- "pve MSI"
|
- name: "m710Q"
|
||||||
- "HP Proxmox"
|
location: "Bureau RdC"
|
||||||
|
- name: "HP Proliant"
|
||||||
|
location: "Bureau RdC"
|
||||||
|
- name: "pve MSI"
|
||||||
|
location: "Bureau RdC"
|
||||||
|
- name: "HP Proxmox"
|
||||||
|
location: "Bureau"
|
||||||
|
- name: "IoT"
|
||||||
|
location: ""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
history:
|
history:
|
||||||
@@ -77,6 +118,11 @@ ui:
|
|||||||
offline_transparency: 0.5 # Transparence des IPs offline
|
offline_transparency: 0.5 # Transparence des IPs offline
|
||||||
show_mac: true
|
show_mac: true
|
||||||
show_vendor: true
|
show_vendor: true
|
||||||
|
cell_size: 30 # Taille des cellules IP en pixels (30, 35, 40...)
|
||||||
|
font_size: 12 # Taille de la police dans les cellules en pixels
|
||||||
|
cell_gap: 2.5 # Espacement entre les cellules en pixels
|
||||||
|
details_font_size: 13 # Taille de la police dans le volet détails en pixels
|
||||||
|
details_spacing: 2 # Espacement entre les champs du volet détails en pixels
|
||||||
|
|
||||||
colors:
|
colors:
|
||||||
free: "#75715E" # IP libre (gris Monokai)
|
free: "#75715E" # IP libre (gris Monokai)
|
||||||
|
|||||||
2405
frontend/package-lock.json
generated
Normal file
2405
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ipwatch-frontend",
|
"name": "ipwatch-frontend",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -9,15 +9,16 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"vue": "^3.4.15",
|
"@mdi/font": "^7.4.47",
|
||||||
|
"axios": "^1.6.5",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"axios": "^1.6.5"
|
"vue": "^3.4.15"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^5.0.3",
|
"@vitejs/plugin-vue": "^5.0.3",
|
||||||
"vite": "^5.0.11",
|
|
||||||
"tailwindcss": "^3.4.1",
|
|
||||||
"autoprefixer": "^10.4.17",
|
"autoprefixer": "^10.4.17",
|
||||||
"postcss": "^8.4.33"
|
"postcss": "^8.4.33",
|
||||||
|
"tailwindcss": "^3.4.1",
|
||||||
|
"vite": "^5.0.11"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-screen flex flex-col bg-monokai-bg">
|
<div class="h-screen flex flex-col bg-monokai-bg">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<AppHeader />
|
<AppHeader @openSettings="showSettings = true" />
|
||||||
|
|
||||||
<!-- Layout 3 colonnes selon consigne-design_webui.md -->
|
<!-- Layout 3 colonnes selon consigne-design_webui.md -->
|
||||||
<div class="flex-1 flex overflow-hidden">
|
<div class="flex-1 flex overflow-hidden">
|
||||||
@@ -20,20 +20,37 @@
|
|||||||
<NewDetections />
|
<NewDetections />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Paramètres -->
|
||||||
|
<SettingsModal
|
||||||
|
:isOpen="showSettings"
|
||||||
|
@close="showSettings = false"
|
||||||
|
@configReloaded="handleConfigReloaded"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, onUnmounted } from 'vue'
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
import { useIPStore } from '@/stores/ipStore'
|
import { useIPStore } from '@/stores/ipStore'
|
||||||
import AppHeader from '@/components/AppHeader.vue'
|
import AppHeader from '@/components/AppHeader.vue'
|
||||||
import IPDetails from '@/components/IPDetails.vue'
|
import IPDetails from '@/components/IPDetails.vue'
|
||||||
import IPGridTree from '@/components/IPGridTree.vue'
|
import IPGridTree from '@/components/IPGridTree.vue'
|
||||||
import NewDetections from '@/components/NewDetections.vue'
|
import NewDetections from '@/components/NewDetections.vue'
|
||||||
|
import SettingsModal from '@/components/SettingsModal.vue'
|
||||||
|
|
||||||
const ipStore = useIPStore()
|
const ipStore = useIPStore()
|
||||||
|
const showSettings = ref(false)
|
||||||
|
|
||||||
|
async function handleConfigReloaded() {
|
||||||
|
// Recharger les IPs après un import ou un rechargement de config
|
||||||
|
await ipStore.fetchIPs()
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
// Charger la configuration UI
|
||||||
|
await ipStore.fetchUIConfig()
|
||||||
|
|
||||||
// Charger les données initiales
|
// Charger les données initiales
|
||||||
await ipStore.fetchIPs()
|
await ipStore.fetchIPs()
|
||||||
|
|
||||||
|
|||||||
@@ -42,17 +42,22 @@ body {
|
|||||||
animation: ping-pulse 1.5s ease-in-out infinite;
|
animation: ping-pulse 1.5s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Grille des IPs avec espacement configurable */
|
||||||
|
.ip-grid {
|
||||||
|
gap: var(--cell-gap, 2px);
|
||||||
|
}
|
||||||
|
|
||||||
/* Cases IP compactes - Version minimale */
|
/* Cases IP compactes - Version minimale */
|
||||||
.ip-cell-compact {
|
.ip-cell-compact {
|
||||||
@apply rounded cursor-pointer transition-all duration-200 relative;
|
@apply rounded cursor-pointer transition-all duration-200 relative;
|
||||||
border: 2px solid;
|
border: 1px solid;
|
||||||
width: 50px;
|
width: var(--cell-size, 30px);
|
||||||
height: 50px;
|
height: var(--cell-size, 30px);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 14px;
|
font-size: var(--font-size, 10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Cases IP - États selon guidelines-css.md */
|
/* Cases IP - États selon guidelines-css.md */
|
||||||
|
|||||||
@@ -4,7 +4,10 @@
|
|||||||
<!-- Logo et titre -->
|
<!-- Logo et titre -->
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<h1 class="text-3xl font-bold text-monokai-green">IPWatch</h1>
|
<h1 class="text-3xl font-bold text-monokai-green">IPWatch</h1>
|
||||||
<span class="text-monokai-comment">Scanner Réseau</span>
|
<div class="flex flex-col">
|
||||||
|
<span class="text-monokai-comment">Scanner Réseau</span>
|
||||||
|
<span class="text-xs text-monokai-comment/60">v{{ appVersion }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Stats et contrôles -->
|
<!-- Stats et contrôles -->
|
||||||
@@ -25,13 +28,37 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Dernier scan -->
|
||||||
|
<div v-if="lastScanDate" class="text-sm text-monokai-comment">
|
||||||
|
Dernier scan: {{ formatScanDate(lastScanDate) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Progression du scan -->
|
||||||
|
<div v-if="isScanning" class="flex items-center gap-2 text-sm">
|
||||||
|
<span class="text-monokai-cyan">
|
||||||
|
{{ scanProgress.current }} / {{ scanProgress.total }}
|
||||||
|
</span>
|
||||||
|
<span v-if="scanProgress.currentIP" class="text-monokai-comment">
|
||||||
|
({{ scanProgress.currentIP }})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Bouton scan -->
|
<!-- Bouton scan -->
|
||||||
<button
|
<button
|
||||||
@click="triggerScan"
|
@click="triggerScan"
|
||||||
:disabled="loading"
|
:disabled="isScanning"
|
||||||
class="px-4 py-2 rounded bg-monokai-cyan text-monokai-bg font-bold hover:bg-monokai-green transition-colors disabled:opacity-50"
|
class="px-4 py-2 rounded bg-monokai-cyan text-monokai-bg font-bold hover:bg-monokai-green transition-colors disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{{ loading ? 'Scan en cours...' : 'Lancer Scan' }}
|
{{ isScanning ? 'Scan en cours...' : 'Lancer Scan' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Bouton Paramètres -->
|
||||||
|
<button
|
||||||
|
@click="openSettings"
|
||||||
|
class="px-4 py-2 rounded bg-monokai-purple text-monokai-bg text-sm hover:bg-monokai-pink transition-colors"
|
||||||
|
title="Paramètres"
|
||||||
|
>
|
||||||
|
⚙ Paramètres
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Indicateur WebSocket -->
|
<!-- Indicateur WebSocket -->
|
||||||
@@ -52,11 +79,29 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { useIPStore } from '@/stores/ipStore'
|
import { useIPStore } from '@/stores/ipStore'
|
||||||
|
|
||||||
|
const emit = defineEmits(['openSettings'])
|
||||||
|
|
||||||
const ipStore = useIPStore()
|
const ipStore = useIPStore()
|
||||||
const { stats, loading, wsConnected } = storeToRefs(ipStore)
|
const { stats, loading, wsConnected, lastScanDate, scanProgress, isScanning } = storeToRefs(ipStore)
|
||||||
|
const appVersion = ref('')
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
// Charger la version depuis le config
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/ips/config/options')
|
||||||
|
if (response.ok) {
|
||||||
|
const config = await response.json()
|
||||||
|
appVersion.value = config.version || '1.0.0'
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur chargement version:', error)
|
||||||
|
appVersion.value = '1.0.0'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
async function triggerScan() {
|
async function triggerScan() {
|
||||||
try {
|
try {
|
||||||
@@ -65,4 +110,26 @@ async function triggerScan() {
|
|||||||
console.error('Erreur lancement scan:', err)
|
console.error('Erreur lancement scan:', err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openSettings() {
|
||||||
|
emit('openSettings')
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatScanDate(date) {
|
||||||
|
if (!date) return ''
|
||||||
|
const d = new Date(date)
|
||||||
|
const now = new Date()
|
||||||
|
const diff = now - d
|
||||||
|
|
||||||
|
if (diff < 60000) return 'il y a quelques secondes'
|
||||||
|
if (diff < 3600000) return `il y a ${Math.floor(diff / 60000)} min`
|
||||||
|
if (diff < 86400000) return `il y a ${Math.floor(diff / 3600000)}h`
|
||||||
|
|
||||||
|
return d.toLocaleString('fr-FR', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -10,18 +10,13 @@
|
|||||||
:title="getTooltip"
|
:title="getTooltip"
|
||||||
>
|
>
|
||||||
<!-- Afficher seulement le dernier octet -->
|
<!-- Afficher seulement le dernier octet -->
|
||||||
<div class="font-mono font-bold text-2xl">
|
<div class="font-mono">
|
||||||
{{ lastOctet }}
|
{{ lastOctet }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Nom très court si connu -->
|
<!-- Indicateur ports ouverts (petit badge décalé et réduit) -->
|
||||||
<div v-if="ip.name" class="text-xs opacity-75 truncate mt-1">
|
|
||||||
{{ ip.name }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Indicateur ports ouverts (petit badge) -->
|
|
||||||
<div v-if="ip.open_ports && ip.open_ports.length > 0"
|
<div v-if="ip.open_ports && ip.open_ports.length > 0"
|
||||||
class="absolute top-1 right-1 w-2 h-2 rounded-full bg-monokai-cyan">
|
class="absolute top-0.5 right-0.5 w-1 h-1 rounded-full bg-monokai-cyan">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,149 +1,215 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full flex flex-col bg-monokai-bg border-r border-monokai-comment">
|
<div
|
||||||
<!-- Header -->
|
class="h-full flex flex-col bg-monokai-bg border-r border-monokai-comment"
|
||||||
<div class="p-4 border-b border-monokai-comment">
|
:style="detailsStyles"
|
||||||
<h2 class="text-xl font-bold text-monokai-cyan">Détails IP</h2>
|
>
|
||||||
</div>
|
<!-- Contenu scrollable -->
|
||||||
|
<div class="flex-1 overflow-hidden flex flex-col">
|
||||||
<!-- Contenu -->
|
<div v-if="selectedIP" class="flex flex-col h-full">
|
||||||
<div class="flex-1 overflow-auto p-4">
|
<!-- Section fixe (sticky) : IP et État sur la même ligne -->
|
||||||
<div v-if="selectedIP" class="space-y-4">
|
<div class="bg-monokai-bg border-b border-monokai-comment" :style="headerPadding">
|
||||||
<!-- Adresse IP -->
|
<div class="flex items-center justify-between gap-2">
|
||||||
<div>
|
<!-- Adresse IP -->
|
||||||
<div class="text-sm text-monokai-comment mb-1">Adresse IP</div>
|
<div class="flex-1 min-w-0">
|
||||||
<div class="text-2xl font-mono font-bold text-monokai-green">
|
<a
|
||||||
{{ selectedIP.ip }}
|
v-if="selectedIP.link"
|
||||||
</div>
|
:href="selectedIP.link"
|
||||||
</div>
|
target="_blank"
|
||||||
|
class="font-mono font-bold text-monokai-green hover:text-monokai-cyan transition-colors cursor-pointer underline block truncate"
|
||||||
<!-- État -->
|
:style="{ fontSize: ipFontSize }"
|
||||||
<div>
|
|
||||||
<div class="text-sm text-monokai-comment mb-1">État</div>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<div
|
|
||||||
:class="[
|
|
||||||
'w-3 h-3 rounded-full',
|
|
||||||
selectedIP.last_status === 'online' ? 'bg-monokai-green' : 'bg-monokai-pink'
|
|
||||||
]"
|
|
||||||
></div>
|
|
||||||
<span class="text-monokai-text capitalize">
|
|
||||||
{{ selectedIP.last_status || 'Inconnu' }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Formulaire d'édition -->
|
|
||||||
<div class="space-y-3 pt-4 border-t border-monokai-comment">
|
|
||||||
<!-- Nom -->
|
|
||||||
<div>
|
|
||||||
<label class="text-sm text-monokai-comment mb-1 block">Nom</label>
|
|
||||||
<input
|
|
||||||
v-model="formData.name"
|
|
||||||
type="text"
|
|
||||||
class="w-full px-3 py-2 bg-monokai-bg border border-monokai-comment rounded text-monokai-text"
|
|
||||||
placeholder="Ex: Serveur Principal"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Connue -->
|
|
||||||
<div>
|
|
||||||
<label class="flex items-center gap-2 cursor-pointer">
|
|
||||||
<input
|
|
||||||
v-model="formData.known"
|
|
||||||
type="checkbox"
|
|
||||||
class="form-checkbox"
|
|
||||||
/>
|
|
||||||
<span class="text-sm text-monokai-text">IP connue</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Localisation -->
|
|
||||||
<div>
|
|
||||||
<label class="text-sm text-monokai-comment mb-1 block">Localisation</label>
|
|
||||||
<input
|
|
||||||
v-model="formData.location"
|
|
||||||
type="text"
|
|
||||||
class="w-full px-3 py-2 bg-monokai-bg border border-monokai-comment rounded text-monokai-text"
|
|
||||||
placeholder="Ex: Bureau"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Type d'hôte -->
|
|
||||||
<div>
|
|
||||||
<label class="text-sm text-monokai-comment mb-1 block">Type d'hôte</label>
|
|
||||||
<input
|
|
||||||
v-model="formData.host"
|
|
||||||
type="text"
|
|
||||||
class="w-full px-3 py-2 bg-monokai-bg border border-monokai-comment rounded text-monokai-text"
|
|
||||||
placeholder="Ex: PC, Serveur, Imprimante"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Boutons -->
|
|
||||||
<div class="flex gap-2 pt-2">
|
|
||||||
<button
|
|
||||||
@click="saveChanges"
|
|
||||||
class="px-4 py-2 bg-monokai-green text-monokai-bg rounded font-bold hover:bg-monokai-cyan transition-colors"
|
|
||||||
>
|
|
||||||
Enregistrer
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
@click="resetForm"
|
|
||||||
class="px-4 py-2 bg-monokai-comment text-monokai-bg rounded hover:bg-monokai-text transition-colors"
|
|
||||||
>
|
|
||||||
Annuler
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Informations réseau -->
|
|
||||||
<div class="pt-4 border-t border-monokai-comment space-y-2">
|
|
||||||
<h3 class="text-monokai-cyan font-bold mb-2">Informations réseau</h3>
|
|
||||||
|
|
||||||
<div v-if="selectedIP.mac">
|
|
||||||
<div class="text-sm text-monokai-comment">MAC</div>
|
|
||||||
<div class="text-monokai-text font-mono">{{ selectedIP.mac }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="selectedIP.vendor">
|
|
||||||
<div class="text-sm text-monokai-comment">Fabricant</div>
|
|
||||||
<div class="text-monokai-text">{{ selectedIP.vendor }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="selectedIP.hostname">
|
|
||||||
<div class="text-sm text-monokai-comment">Hostname</div>
|
|
||||||
<div class="text-monokai-text font-mono">{{ selectedIP.hostname }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="selectedIP.open_ports && selectedIP.open_ports.length > 0">
|
|
||||||
<div class="text-sm text-monokai-comment">Ports ouverts</div>
|
|
||||||
<div class="flex flex-wrap gap-1 mt-1">
|
|
||||||
<span
|
|
||||||
v-for="port in selectedIP.open_ports"
|
|
||||||
:key="port"
|
|
||||||
class="px-2 py-1 bg-monokai-cyan/20 text-monokai-cyan rounded text-xs font-mono"
|
|
||||||
>
|
>
|
||||||
{{ port }}
|
{{ selectedIP.ip }}
|
||||||
|
</a>
|
||||||
|
<div v-else class="font-mono font-bold text-monokai-green truncate" :style="{ fontSize: ipFontSize }">
|
||||||
|
{{ selectedIP.ip }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- État -->
|
||||||
|
<div class="flex items-center gap-1 flex-shrink-0">
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
'w-2 h-2 rounded-full',
|
||||||
|
selectedIP.last_status === 'online' ? 'bg-monokai-green' : 'bg-monokai-pink'
|
||||||
|
]"
|
||||||
|
></div>
|
||||||
|
<span class="text-monokai-text capitalize text-xs">
|
||||||
|
{{ selectedIP.last_status || 'Inconnu' }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Timestamps -->
|
<!-- Section scrollable : Formulaire et reste -->
|
||||||
<div class="pt-4 border-t border-monokai-comment space-y-2 text-sm">
|
<div class="flex-1 overflow-auto" :style="contentPadding">
|
||||||
<div v-if="selectedIP.first_seen">
|
<!-- Formulaire d'édition -->
|
||||||
<span class="text-monokai-comment">Première vue:</span>
|
<div :style="formSpacing">
|
||||||
<span class="text-monokai-text ml-2">{{ formatDate(selectedIP.first_seen) }}</span>
|
<!-- Nom -->
|
||||||
|
<div>
|
||||||
|
<label class="text-sm text-monokai-comment mb-1 block">Nom</label>
|
||||||
|
<input
|
||||||
|
v-model="formData.name"
|
||||||
|
type="text"
|
||||||
|
class="w-full px-3 py-2 bg-monokai-bg border border-monokai-comment rounded text-monokai-text"
|
||||||
|
placeholder="Ex: Serveur Principal"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Connue -->
|
||||||
|
<div>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
v-model="formData.known"
|
||||||
|
type="checkbox"
|
||||||
|
class="form-checkbox"
|
||||||
|
/>
|
||||||
|
<span class="text-sm text-monokai-text">IP connue</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Type d'hôte -->
|
||||||
|
<div>
|
||||||
|
<label class="text-sm text-monokai-comment mb-1 block">Type d'hôte</label>
|
||||||
|
<select
|
||||||
|
v-model="formData.host"
|
||||||
|
@change="onHostChange"
|
||||||
|
class="w-full px-3 py-2 bg-monokai-bg border border-monokai-comment rounded text-monokai-text"
|
||||||
|
>
|
||||||
|
<option value="">-- Sélectionner --</option>
|
||||||
|
<option v-for="host in hosts" :key="host.name" :value="host.name">{{ host.name }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Localisation -->
|
||||||
|
<div>
|
||||||
|
<label class="text-sm text-monokai-comment mb-1 block">Localisation</label>
|
||||||
|
<select
|
||||||
|
v-model="formData.location"
|
||||||
|
class="w-full px-3 py-2 bg-monokai-bg border border-monokai-comment rounded text-monokai-text"
|
||||||
|
>
|
||||||
|
<option value="">-- Sélectionner --</option>
|
||||||
|
<option v-for="loc in locations" :key="loc" :value="loc">{{ loc }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Lien -->
|
||||||
|
<div>
|
||||||
|
<label class="text-sm text-monokai-comment mb-1 block">Lien (URL)</label>
|
||||||
|
<input
|
||||||
|
v-model="formData.link"
|
||||||
|
type="url"
|
||||||
|
class="w-full px-3 py-2 bg-monokai-bg border border-monokai-comment rounded text-monokai-text"
|
||||||
|
placeholder="https://exemple.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div v-if="selectedIP.last_seen">
|
|
||||||
<span class="text-monokai-comment">Dernière vue:</span>
|
<!-- Informations réseau -->
|
||||||
<span class="text-monokai-text ml-2">{{ formatDate(selectedIP.last_seen) }}</span>
|
<div class="border-t border-monokai-comment" :style="{ paddingTop: `${uiConfig.details_spacing * 2}px`, marginTop: `${uiConfig.details_spacing * 2}px` }">
|
||||||
|
<div :style="formSpacing">
|
||||||
|
<h3 class="text-monokai-cyan font-bold mb-2">Informations réseau</h3>
|
||||||
|
|
||||||
|
<div v-if="selectedIP.mac">
|
||||||
|
<div class="text-sm text-monokai-comment">MAC</div>
|
||||||
|
<div class="text-monokai-text font-mono">{{ selectedIP.mac }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="selectedIP.vendor">
|
||||||
|
<div class="text-sm text-monokai-comment">Fabricant</div>
|
||||||
|
<div class="text-monokai-text">{{ selectedIP.vendor }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="selectedIP.hostname">
|
||||||
|
<div class="text-sm text-monokai-comment">Hostname</div>
|
||||||
|
<div class="text-monokai-text font-mono">{{ selectedIP.hostname }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="selectedIP.open_ports && selectedIP.open_ports.length > 0">
|
||||||
|
<div class="text-sm text-monokai-comment">Ports ouverts</div>
|
||||||
|
<div class="flex flex-wrap gap-1 mt-1">
|
||||||
|
<a
|
||||||
|
v-for="port in selectedIP.open_ports"
|
||||||
|
:key="port"
|
||||||
|
:href="getPortUrl(port)"
|
||||||
|
:target="getPortUrl(port) ? '_blank' : undefined"
|
||||||
|
:class="[
|
||||||
|
'px-2 py-1 rounded text-xs font-mono',
|
||||||
|
getPortUrl(port)
|
||||||
|
? 'bg-monokai-cyan/20 text-monokai-cyan hover:bg-monokai-cyan/30 cursor-pointer underline'
|
||||||
|
: 'bg-monokai-cyan/20 text-monokai-cyan cursor-default'
|
||||||
|
]"
|
||||||
|
:title="getPortUrl(port) ? `Ouvrir ${portProtocols[port] || portProtocols[String(port)]}://${selectedIP.ip}:${port}` : `Port ${port}`"
|
||||||
|
>
|
||||||
|
{{ port }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Boutons d'action avec icônes -->
|
||||||
|
<div class="flex gap-2 justify-center" :style="{ marginTop: `${uiConfig.details_spacing * 3}px` }">
|
||||||
|
<button
|
||||||
|
@click="saveChanges"
|
||||||
|
:class="[
|
||||||
|
'p-2 rounded transition-colors',
|
||||||
|
saveButtonState === 'saved'
|
||||||
|
? 'bg-green-800 text-monokai-text'
|
||||||
|
: 'bg-monokai-green text-monokai-bg hover:bg-monokai-cyan'
|
||||||
|
]"
|
||||||
|
:disabled="saveButtonState === 'saving'"
|
||||||
|
title="Enregistrer"
|
||||||
|
>
|
||||||
|
<span class="mdi mdi-content-save text-xl"></span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="scanPorts"
|
||||||
|
:class="[
|
||||||
|
'p-2 rounded transition-colors',
|
||||||
|
portScanState === 'scanning'
|
||||||
|
? 'bg-monokai-orange text-monokai-bg'
|
||||||
|
: 'bg-monokai-cyan text-monokai-bg hover:bg-monokai-purple'
|
||||||
|
]"
|
||||||
|
:disabled="portScanState === 'scanning'"
|
||||||
|
title="Scanner les ports"
|
||||||
|
>
|
||||||
|
<span class="mdi mdi-wifi-settings text-xl"></span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="resetForm"
|
||||||
|
class="p-2 bg-monokai-comment text-monokai-bg rounded hover:bg-monokai-text transition-colors"
|
||||||
|
title="Annuler"
|
||||||
|
>
|
||||||
|
<span class="mdi mdi-close-box text-xl"></span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="deleteIP"
|
||||||
|
class="p-2 bg-monokai-pink text-monokai-bg rounded hover:bg-red-700 transition-colors"
|
||||||
|
title="Effacer"
|
||||||
|
>
|
||||||
|
<span class="mdi mdi-eraser text-xl"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Timestamps -->
|
||||||
|
<div class="border-t border-monokai-comment text-sm" :style="{ paddingTop: `${uiConfig.details_spacing * 2}px`, marginTop: `${uiConfig.details_spacing * 2}px` }">
|
||||||
|
<div :style="formSpacing">
|
||||||
|
<div v-if="selectedIP.first_seen">
|
||||||
|
<span class="text-monokai-comment">Première vue:</span>
|
||||||
|
<span class="text-monokai-text ml-2">{{ formatDate(selectedIP.first_seen) }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="selectedIP.last_seen">
|
||||||
|
<span class="text-monokai-comment">Dernière vue:</span>
|
||||||
|
<span class="text-monokai-text ml-2">{{ formatDate(selectedIP.last_seen) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Placeholder si rien sélectionné -->
|
<!-- Placeholder si rien sélectionné -->
|
||||||
<div v-else class="text-center text-monokai-comment mt-10">
|
<div v-else class="text-center text-monokai-comment mt-10 p-4">
|
||||||
<p>Sélectionnez une IP pour voir les détails</p>
|
<p>Sélectionnez une IP pour voir les détails</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -151,39 +217,118 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch, onMounted, computed } from 'vue'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { useIPStore } from '@/stores/ipStore'
|
import { useIPStore } from '@/stores/ipStore'
|
||||||
|
|
||||||
const ipStore = useIPStore()
|
const ipStore = useIPStore()
|
||||||
const { selectedIP } = storeToRefs(ipStore)
|
const { selectedIP } = storeToRefs(ipStore)
|
||||||
|
|
||||||
|
// Données de configuration depuis config.yaml (chargées dynamiquement)
|
||||||
|
const locations = ref([])
|
||||||
|
const hosts = ref([])
|
||||||
|
const portProtocols = ref({}) // Mapping port -> protocole
|
||||||
|
const uiConfig = ref({
|
||||||
|
details_font_size: 13,
|
||||||
|
details_spacing: 2
|
||||||
|
})
|
||||||
|
|
||||||
const formData = ref({
|
const formData = ref({
|
||||||
name: '',
|
name: '',
|
||||||
known: false,
|
known: false,
|
||||||
location: '',
|
location: '',
|
||||||
host: ''
|
host: '',
|
||||||
|
link: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// État du bouton enregistrer
|
||||||
|
const saveButtonState = ref('idle') // 'idle', 'saving', 'saved'
|
||||||
|
|
||||||
|
// État du scan de ports
|
||||||
|
const portScanState = ref('idle') // 'idle', 'scanning'
|
||||||
|
|
||||||
|
// Styles dynamiques basés sur la config
|
||||||
|
const detailsStyles = computed(() => ({
|
||||||
|
fontSize: `${uiConfig.value.details_font_size}px`
|
||||||
|
}))
|
||||||
|
|
||||||
|
const formSpacing = computed(() => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: `${uiConfig.value.details_spacing}px`
|
||||||
|
}))
|
||||||
|
|
||||||
|
const headerPadding = computed(() => ({
|
||||||
|
padding: `${uiConfig.value.details_spacing * 2}px ${uiConfig.value.details_spacing * 3}px`
|
||||||
|
}))
|
||||||
|
|
||||||
|
const contentPadding = computed(() => ({
|
||||||
|
padding: `${uiConfig.value.details_spacing * 3}px`
|
||||||
|
}))
|
||||||
|
|
||||||
|
const ipFontSize = computed(() => `${uiConfig.value.details_font_size * 1.2}px`)
|
||||||
|
|
||||||
|
// Charger les options de configuration au montage
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/ips/config/options')
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json()
|
||||||
|
locations.value = data.locations || []
|
||||||
|
hosts.value = data.hosts || []
|
||||||
|
portProtocols.value = data.port_protocols || {}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur chargement options config:', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Charger la config UI depuis le store
|
||||||
|
if (ipStore.uiConfig) {
|
||||||
|
uiConfig.value = {
|
||||||
|
details_font_size: ipStore.uiConfig.details_font_size || 13,
|
||||||
|
details_spacing: ipStore.uiConfig.details_spacing || 2
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Mettre à jour le formulaire quand l'IP change
|
// Mettre à jour le formulaire quand l'IP change
|
||||||
watch(selectedIP, (newIP) => {
|
watch(selectedIP, (newIP) => {
|
||||||
if (newIP) {
|
if (newIP) {
|
||||||
|
// Auto-remplir l'URL si vide et port 80 ouvert
|
||||||
|
let autoLink = newIP.link || ''
|
||||||
|
if (!autoLink && newIP.open_ports && newIP.open_ports.includes(80)) {
|
||||||
|
autoLink = `http://${newIP.ip}`
|
||||||
|
}
|
||||||
|
|
||||||
formData.value = {
|
formData.value = {
|
||||||
name: newIP.name || '',
|
name: newIP.name || '',
|
||||||
known: newIP.known || false,
|
known: newIP.known || false,
|
||||||
location: newIP.location || '',
|
location: newIP.location || '',
|
||||||
host: newIP.host || ''
|
host: newIP.host || '',
|
||||||
|
link: autoLink
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Réinitialiser l'état du bouton
|
||||||
|
saveButtonState.value = 'idle'
|
||||||
}
|
}
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
|
// Auto-remplir la localisation quand l'hôte change
|
||||||
|
function onHostChange() {
|
||||||
|
const selectedHost = hosts.value.find(h => h.name === formData.value.host)
|
||||||
|
if (selectedHost && selectedHost.location) {
|
||||||
|
formData.value.location = selectedHost.location
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function resetForm() {
|
function resetForm() {
|
||||||
if (selectedIP.value) {
|
if (selectedIP.value) {
|
||||||
formData.value = {
|
formData.value = {
|
||||||
name: selectedIP.value.name || '',
|
name: selectedIP.value.name || '',
|
||||||
known: selectedIP.value.known || false,
|
known: selectedIP.value.known || false,
|
||||||
location: selectedIP.value.location || '',
|
location: selectedIP.value.location || '',
|
||||||
host: selectedIP.value.host || ''
|
host: selectedIP.value.host || '',
|
||||||
|
link: selectedIP.value.link || ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -192,10 +337,73 @@ async function saveChanges() {
|
|||||||
if (!selectedIP.value) return
|
if (!selectedIP.value) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
saveButtonState.value = 'saving'
|
||||||
await ipStore.updateIP(selectedIP.value.ip, formData.value)
|
await ipStore.updateIP(selectedIP.value.ip, formData.value)
|
||||||
|
saveButtonState.value = 'saved'
|
||||||
console.log('IP mise à jour')
|
console.log('IP mise à jour')
|
||||||
|
|
||||||
|
// Retour à l'état normal après 2 secondes
|
||||||
|
setTimeout(() => {
|
||||||
|
saveButtonState.value = 'idle'
|
||||||
|
}, 2000)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Erreur mise à jour IP:', err)
|
console.error('Erreur mise à jour IP:', err)
|
||||||
|
saveButtonState.value = 'idle'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function scanPorts() {
|
||||||
|
if (!selectedIP.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
portScanState.value = 'scanning'
|
||||||
|
const response = await fetch(`/api/scan/ports/${selectedIP.value.ip}`, {
|
||||||
|
method: 'POST'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Erreur lors du scan de ports')
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json()
|
||||||
|
console.log('Scan de ports terminé:', result)
|
||||||
|
|
||||||
|
// Rafraîchir les données de l'IP
|
||||||
|
await ipStore.fetchIPs()
|
||||||
|
|
||||||
|
portScanState.value = 'idle'
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Erreur scan ports:', err)
|
||||||
|
portScanState.value = 'idle'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteIP() {
|
||||||
|
if (!selectedIP.value) return
|
||||||
|
|
||||||
|
if (confirm(`Voulez-vous vraiment effacer les données de ${selectedIP.value.ip} ?`)) {
|
||||||
|
try {
|
||||||
|
// Effacer les données et passer en offline-unknown
|
||||||
|
const resetData = {
|
||||||
|
name: '',
|
||||||
|
known: false,
|
||||||
|
location: '',
|
||||||
|
host: '',
|
||||||
|
link: '',
|
||||||
|
last_status: 'offline'
|
||||||
|
}
|
||||||
|
await ipStore.updateIP(selectedIP.value.ip, resetData)
|
||||||
|
formData.value = {
|
||||||
|
name: '',
|
||||||
|
known: false,
|
||||||
|
location: '',
|
||||||
|
host: '',
|
||||||
|
link: ''
|
||||||
|
}
|
||||||
|
console.log('IP effacée')
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Erreur effacement IP:', err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,4 +412,28 @@ function formatDate(dateString) {
|
|||||||
const date = new Date(dateString)
|
const date = new Date(dateString)
|
||||||
return date.toLocaleString('fr-FR')
|
return date.toLocaleString('fr-FR')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Générer une URL cliquable pour un port
|
||||||
|
function getPortUrl(port) {
|
||||||
|
if (!selectedIP.value) return null
|
||||||
|
|
||||||
|
// Chercher le protocole (la clé peut être un nombre ou une string)
|
||||||
|
const protocol = portProtocols.value[port] || portProtocols.value[String(port)]
|
||||||
|
if (!protocol) return null
|
||||||
|
|
||||||
|
const ip = selectedIP.value.ip
|
||||||
|
|
||||||
|
// Mapping protocole -> URL
|
||||||
|
const urlMap = {
|
||||||
|
'http': `http://${ip}:${port}`,
|
||||||
|
'https': `https://${ip}:${port}`,
|
||||||
|
'ssh': `ssh://${ip}:${port}`,
|
||||||
|
'smb': `smb://${ip}`,
|
||||||
|
'rdp': `rdp://${ip}:${port}`,
|
||||||
|
'mysql': `mysql://${ip}:${port}`,
|
||||||
|
'postgresql': `postgresql://${ip}:${port}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return urlMap[protocol] || null
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -24,18 +24,30 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Grille d'IPs -->
|
<!-- Grille d'IPs par sous-réseaux -->
|
||||||
<div class="flex-1 overflow-auto p-4">
|
<div class="flex-1 overflow-auto p-4">
|
||||||
<div class="grid grid-cols-4 gap-3">
|
<!-- Pour chaque sous-réseau -->
|
||||||
<IPCell
|
<div v-for="subnet in groupedSubnets" :key="subnet.name" class="mb-6">
|
||||||
v-for="ip in filteredIPs"
|
<!-- En-tête de section -->
|
||||||
:key="ip.ip"
|
<div class="mb-3 pb-2 border-b-2 border-monokai-cyan/30">
|
||||||
:ip="ip"
|
<p class="text-xs text-monokai-comment">{{ subnet.name }}</p>
|
||||||
/>
|
<h3 v-if="subnet.start && subnet.end" class="text-lg font-bold text-monokai-cyan mt-0.5">
|
||||||
|
{{ subnet.start }} à {{ subnet.end }}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Grille des IPs de ce sous-réseau -->
|
||||||
|
<div class="grid grid-cols-4 gap-3">
|
||||||
|
<IPCell
|
||||||
|
v-for="ip in subnet.ips"
|
||||||
|
:key="ip.ip"
|
||||||
|
:ip="ip"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Message si vide -->
|
<!-- Message si vide -->
|
||||||
<div v-if="filteredIPs.length === 0" class="text-center text-monokai-comment mt-10">
|
<div v-if="groupedSubnets.length === 0" class="text-center text-monokai-comment mt-10">
|
||||||
<p>Aucune IP à afficher</p>
|
<p>Aucune IP à afficher</p>
|
||||||
<p class="text-sm mt-2">Ajustez les filtres ou lancez un scan</p>
|
<p class="text-sm mt-2">Ajustez les filtres ou lancez un scan</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,10 +82,98 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { useIPStore } from '@/stores/ipStore'
|
import { useIPStore } from '@/stores/ipStore'
|
||||||
import IPCell from './IPCell.vue'
|
import IPCell from './IPCell.vue'
|
||||||
|
|
||||||
const ipStore = useIPStore()
|
const ipStore = useIPStore()
|
||||||
const { filteredIPs, filters } = storeToRefs(ipStore)
|
const { filteredIPs, filters } = storeToRefs(ipStore)
|
||||||
|
|
||||||
|
// Subnets depuis la config
|
||||||
|
const subnets = ref([])
|
||||||
|
|
||||||
|
// Charger les subnets depuis la config
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/ips/config/content')
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json()
|
||||||
|
// Parser le YAML pour extraire les subnets
|
||||||
|
const yamlContent = data.content
|
||||||
|
const subnetMatches = yamlContent.match(/subnets:[\s\S]*?(?=\n\w+:|\n$)/)?.[0]
|
||||||
|
|
||||||
|
if (subnetMatches) {
|
||||||
|
// Simple parsing des subnets (améliorer si nécessaire)
|
||||||
|
const subnetLines = subnetMatches.split('\n')
|
||||||
|
let currentSubnet = null
|
||||||
|
|
||||||
|
subnetLines.forEach(line => {
|
||||||
|
if (line.includes('- name:')) {
|
||||||
|
if (currentSubnet) subnets.value.push(currentSubnet)
|
||||||
|
currentSubnet = { name: line.split('"')[1] }
|
||||||
|
} else if (currentSubnet) {
|
||||||
|
if (line.includes('start:')) currentSubnet.start = line.split('"')[1]
|
||||||
|
if (line.includes('end:')) currentSubnet.end = line.split('"')[1]
|
||||||
|
if (line.includes('cidr:')) currentSubnet.cidr = line.split('"')[1]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (currentSubnet) subnets.value.push(currentSubnet)
|
||||||
|
console.log('=== SUBNETS LOADED V2 ===', Date.now(), subnets.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur chargement subnets:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Fonction pour vérifier si une IP appartient à un subnet
|
||||||
|
function ipInSubnet(ip, start, end) {
|
||||||
|
const ipNum = ipToNumber(ip)
|
||||||
|
const startNum = ipToNumber(start)
|
||||||
|
const endNum = ipToNumber(end)
|
||||||
|
return ipNum >= startNum && ipNum <= endNum
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convertir une IP en nombre
|
||||||
|
function ipToNumber(ip) {
|
||||||
|
return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet), 0) >>> 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grouper les IPs par subnet
|
||||||
|
const groupedSubnets = computed(() => {
|
||||||
|
const groups = []
|
||||||
|
|
||||||
|
// Pour chaque subnet défini
|
||||||
|
subnets.value.forEach(subnet => {
|
||||||
|
const subnetIPs = filteredIPs.value.filter(ip =>
|
||||||
|
ipInSubnet(ip.ip, subnet.start, subnet.end)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (subnetIPs.length > 0) {
|
||||||
|
groups.push({
|
||||||
|
name: subnet.name,
|
||||||
|
start: subnet.start,
|
||||||
|
end: subnet.end,
|
||||||
|
ips: subnetIPs.sort((a, b) => ipToNumber(a.ip) - ipToNumber(b.ip))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Ajouter section "Autres" pour les IPs hors subnets
|
||||||
|
const otherIPs = filteredIPs.value.filter(ip => {
|
||||||
|
return !subnets.value.some(subnet => ipInSubnet(ip.ip, subnet.start, subnet.end))
|
||||||
|
})
|
||||||
|
|
||||||
|
if (otherIPs.length > 0) {
|
||||||
|
groups.push({
|
||||||
|
name: 'Autres',
|
||||||
|
start: '',
|
||||||
|
end: '',
|
||||||
|
ips: otherIPs.sort((a, b) => ipToNumber(a.ip) - ipToNumber(b.ip))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -38,11 +38,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Grille compacte des IPs du sous-réseau -->
|
<!-- Grille compacte des IPs du sous-réseau -->
|
||||||
<div class="flex flex-wrap gap-2 pl-8">
|
<div class="flex flex-wrap pl-8 ip-grid">
|
||||||
<IPCell
|
<IPCell
|
||||||
v-for="ip in subnet.ips"
|
v-for="ip in subnet.ips"
|
||||||
:key="ip.ip"
|
:key="ip.ip"
|
||||||
:ip="ip"
|
:ip="ip"
|
||||||
|
:is-pinging="scanProgress.currentIP === ip.ip"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -89,7 +90,7 @@ import { useIPStore } from '@/stores/ipStore'
|
|||||||
import IPCell from './IPCell.vue'
|
import IPCell from './IPCell.vue'
|
||||||
|
|
||||||
const ipStore = useIPStore()
|
const ipStore = useIPStore()
|
||||||
const { filteredIPs, filters } = storeToRefs(ipStore)
|
const { filteredIPs, filters, scanProgress } = storeToRefs(ipStore)
|
||||||
|
|
||||||
// Définition des sous-réseaux (devrait venir de la config mais en dur pour l'instant)
|
// Définition des sous-réseaux (devrait venir de la config mais en dur pour l'instant)
|
||||||
const subnets = [
|
const subnets = [
|
||||||
@@ -98,6 +99,21 @@ const subnets = [
|
|||||||
{ name: 'iot', cidr: '10.0.2.0/24', description: 'IoT' }
|
{ name: 'iot', cidr: '10.0.2.0/24', description: 'IoT' }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// Fonction pour trier les IPs par ordre numérique
|
||||||
|
function sortIPsNumerically(ips) {
|
||||||
|
return ips.slice().sort((a, b) => {
|
||||||
|
const partsA = a.ip.split('.').map(Number)
|
||||||
|
const partsB = b.ip.split('.').map(Number)
|
||||||
|
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
if (partsA[i] !== partsB[i]) {
|
||||||
|
return partsA[i] - partsB[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Organiser les IPs par sous-réseau
|
// Organiser les IPs par sous-réseau
|
||||||
const organizedSubnets = computed(() => {
|
const organizedSubnets = computed(() => {
|
||||||
return subnets.map(subnet => {
|
return subnets.map(subnet => {
|
||||||
@@ -110,18 +126,21 @@ const organizedSubnets = computed(() => {
|
|||||||
return ip.ip.startsWith(prefix + '.')
|
return ip.ip.startsWith(prefix + '.')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Trier par ordre numérique
|
||||||
|
const sortedIPs = sortIPsNumerically(subnetIPs)
|
||||||
|
|
||||||
// Calculer les stats
|
// Calculer les stats
|
||||||
const stats = {
|
const stats = {
|
||||||
total: subnetIPs.length,
|
total: sortedIPs.length,
|
||||||
online: subnetIPs.filter(ip => ip.last_status === 'online').length,
|
online: sortedIPs.filter(ip => ip.last_status === 'online').length,
|
||||||
offline: subnetIPs.filter(ip => ip.last_status === 'offline').length
|
offline: sortedIPs.filter(ip => ip.last_status === 'offline').length
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: subnet.name,
|
name: subnet.name,
|
||||||
cidr: subnet.cidr,
|
cidr: subnet.cidr,
|
||||||
description: subnet.description,
|
description: subnet.description,
|
||||||
ips: subnetIPs,
|
ips: sortedIPs,
|
||||||
stats
|
stats
|
||||||
}
|
}
|
||||||
}).filter(subnet => subnet.ips.length > 0) // Ne montrer que les sous-réseaux avec des IPs
|
}).filter(subnet => subnet.ips.length > 0) // Ne montrer que les sous-réseaux avec des IPs
|
||||||
|
|||||||
@@ -7,46 +7,51 @@
|
|||||||
|
|
||||||
<!-- Liste -->
|
<!-- Liste -->
|
||||||
<div class="flex-1 overflow-auto p-4">
|
<div class="flex-1 overflow-auto p-4">
|
||||||
<div v-if="newIPs.length > 0" class="space-y-3">
|
<div v-if="newIPs.length > 0" class="space-y-2">
|
||||||
<div
|
<div
|
||||||
v-for="ip in newIPs"
|
v-for="ip in newIPs"
|
||||||
:key="ip.ip"
|
:key="ip.ip"
|
||||||
@click="selectIP(ip)"
|
@click="selectIP(ip)"
|
||||||
class="p-3 rounded border-2 border-monokai-pink bg-monokai-pink/10 cursor-pointer hover:bg-monokai-pink/20 transition-colors"
|
:class="[
|
||||||
|
'p-2 rounded border-2 cursor-pointer transition-colors',
|
||||||
|
isOlderThanOneHour(ip.first_seen)
|
||||||
|
? 'border-monokai-purple-dark bg-monokai-purple-dark/10 hover:bg-monokai-purple-dark/20'
|
||||||
|
: 'border-monokai-pink bg-monokai-pink/10 hover:bg-monokai-pink/20'
|
||||||
|
]"
|
||||||
>
|
>
|
||||||
<!-- IP -->
|
<!-- IP + Statut sur la même ligne -->
|
||||||
<div class="font-mono font-bold text-monokai-text">
|
<div class="flex items-center justify-between">
|
||||||
{{ ip.ip }}
|
<span class="font-mono font-bold text-monokai-text text-sm">
|
||||||
|
{{ ip.ip }}
|
||||||
|
</span>
|
||||||
|
<div class="flex items-center gap-2 text-xs">
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
'px-2 py-0.5 rounded',
|
||||||
|
ip.last_status === 'online'
|
||||||
|
? 'bg-monokai-green/20 text-monokai-green'
|
||||||
|
: 'bg-monokai-comment/20 text-monokai-comment'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ ip.last_status || 'Inconnu' }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="!ip.known"
|
||||||
|
class="px-2 py-0.5 rounded bg-monokai-cyan/20 text-monokai-cyan"
|
||||||
|
>
|
||||||
|
Inconnue
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- État -->
|
<!-- MAC et Vendor (si disponible) -->
|
||||||
<div class="text-sm mt-1">
|
<div v-if="ip.mac" class="text-xs text-monokai-comment mt-1 font-mono">
|
||||||
<span
|
|
||||||
:class="[
|
|
||||||
'px-2 py-1 rounded text-xs',
|
|
||||||
ip.last_status === 'online'
|
|
||||||
? 'bg-monokai-green/20 text-monokai-green'
|
|
||||||
: 'bg-monokai-comment/20 text-monokai-comment'
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
{{ ip.last_status || 'Inconnu' }}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
v-if="!ip.known"
|
|
||||||
class="ml-2 px-2 py-1 rounded text-xs bg-monokai-purple/20 text-monokai-purple"
|
|
||||||
>
|
|
||||||
Inconnue
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- MAC et Vendor -->
|
|
||||||
<div v-if="ip.mac" class="text-xs text-monokai-comment mt-2 font-mono">
|
|
||||||
{{ ip.mac }}
|
{{ ip.mac }}
|
||||||
<span v-if="ip.vendor" class="ml-1">({{ ip.vendor }})</span>
|
<span v-if="ip.vendor" class="ml-1">({{ ip.vendor }})</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Timestamp -->
|
<!-- Timestamp -->
|
||||||
<div class="text-xs text-monokai-comment mt-2">
|
<div class="text-xs text-monokai-comment mt-1">
|
||||||
{{ formatTime(ip.first_seen) }}
|
{{ formatTime(ip.first_seen) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -69,15 +74,21 @@ import { useIPStore } from '@/stores/ipStore'
|
|||||||
const ipStore = useIPStore()
|
const ipStore = useIPStore()
|
||||||
const { ips } = storeToRefs(ipStore)
|
const { ips } = storeToRefs(ipStore)
|
||||||
|
|
||||||
// IPs nouvellement détectées (dans les dernières 24h)
|
// IPs nouvellement détectées (dans les dernières 24h ET online ET non enregistrées)
|
||||||
const newIPs = computed(() => {
|
const newIPs = computed(() => {
|
||||||
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000)
|
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000)
|
||||||
|
|
||||||
return ips.value
|
return ips.value
|
||||||
.filter(ip => {
|
.filter(ip => {
|
||||||
|
// Doit être online
|
||||||
|
if (ip.last_status !== 'online') return false
|
||||||
|
// Doit avoir un first_seen récent
|
||||||
if (!ip.first_seen) return false
|
if (!ip.first_seen) return false
|
||||||
const firstSeen = new Date(ip.first_seen)
|
const firstSeen = new Date(ip.first_seen)
|
||||||
return firstSeen > oneDayAgo
|
if (firstSeen <= oneDayAgo) return false
|
||||||
|
// Ne pas afficher les IPs déjà enregistrées (avec nom ou connue)
|
||||||
|
if (ip.known || ip.name) return false
|
||||||
|
return true
|
||||||
})
|
})
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
const dateA = new Date(a.first_seen)
|
const dateA = new Date(a.first_seen)
|
||||||
@@ -91,6 +102,15 @@ function selectIP(ip) {
|
|||||||
ipStore.selectIP(ip)
|
ipStore.selectIP(ip)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vérifier si la détection date de plus d'une heure
|
||||||
|
function isOlderThanOneHour(dateString) {
|
||||||
|
if (!dateString) return false
|
||||||
|
const date = new Date(dateString)
|
||||||
|
const now = new Date()
|
||||||
|
const diff = now - date
|
||||||
|
return diff >= 3600000 // 1 heure = 3600000 ms
|
||||||
|
}
|
||||||
|
|
||||||
function formatTime(dateString) {
|
function formatTime(dateString) {
|
||||||
if (!dateString) return ''
|
if (!dateString) return ''
|
||||||
const date = new Date(dateString)
|
const date = new Date(dateString)
|
||||||
|
|||||||
193
frontend/src/components/SettingsModal.vue
Normal file
193
frontend/src/components/SettingsModal.vue
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="isOpen"
|
||||||
|
class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
|
||||||
|
@click.self="close"
|
||||||
|
>
|
||||||
|
<div class="bg-monokai-bg border-2 border-monokai-cyan rounded-lg w-[800px] max-h-[80vh] flex flex-col">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="p-4 border-b border-monokai-comment flex justify-between items-center">
|
||||||
|
<h2 class="text-xl font-bold text-monokai-cyan">Paramètres</h2>
|
||||||
|
<button
|
||||||
|
@click="close"
|
||||||
|
class="text-monokai-comment hover:text-monokai-text text-2xl leading-none"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<div class="flex-1 overflow-auto p-4 space-y-4">
|
||||||
|
<!-- Boutons d'action -->
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button
|
||||||
|
@click="reloadConfig"
|
||||||
|
:disabled="loading"
|
||||||
|
class="px-4 py-2 bg-monokai-green text-monokai-bg rounded hover:bg-monokai-cyan transition-colors disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{{ loading ? 'Chargement...' : 'Recharger Config' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<label class="px-4 py-2 bg-monokai-cyan text-monokai-bg rounded hover:bg-monokai-green transition-colors cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept=".xml"
|
||||||
|
@change="importIpscan"
|
||||||
|
class="hidden"
|
||||||
|
/>
|
||||||
|
Importer IPScan XML
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Messages -->
|
||||||
|
<div v-if="message" :class="['p-3 rounded', messageClass]">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Contenu config.yaml -->
|
||||||
|
<div>
|
||||||
|
<h3 class="text-monokai-cyan font-bold mb-2">Fichier config.yaml</h3>
|
||||||
|
<pre class="bg-monokai-bg border border-monokai-comment rounded p-4 text-sm text-monokai-text overflow-auto max-h-[400px] font-mono">{{ configContent }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="p-4 border-t border-monokai-comment flex justify-end">
|
||||||
|
<button
|
||||||
|
@click="close"
|
||||||
|
class="px-4 py-2 bg-monokai-comment text-monokai-bg rounded hover:bg-monokai-text transition-colors"
|
||||||
|
>
|
||||||
|
Fermer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
isOpen: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['close', 'configReloaded'])
|
||||||
|
|
||||||
|
const configContent = ref('')
|
||||||
|
const loading = ref(false)
|
||||||
|
const message = ref('')
|
||||||
|
const messageType = ref('info')
|
||||||
|
|
||||||
|
// Computed class pour le message
|
||||||
|
const messageClass = ref('')
|
||||||
|
|
||||||
|
watch(() => props.isOpen, async (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
await loadConfigContent()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
async function loadConfigContent() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/ips/config/content')
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json()
|
||||||
|
configContent.value = data.content
|
||||||
|
} else {
|
||||||
|
configContent.value = 'Erreur de chargement du fichier config.yaml'
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur chargement config:', error)
|
||||||
|
configContent.value = 'Erreur de chargement du fichier config.yaml'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reloadConfig() {
|
||||||
|
loading.value = true
|
||||||
|
message.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/ips/config/reload', {
|
||||||
|
method: 'POST'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json()
|
||||||
|
message.value = data.message
|
||||||
|
messageType.value = 'success'
|
||||||
|
messageClass.value = 'bg-monokai-green/20 text-monokai-green border border-monokai-green'
|
||||||
|
emit('configReloaded')
|
||||||
|
|
||||||
|
// Recharger le contenu du config
|
||||||
|
await loadConfigContent()
|
||||||
|
} else {
|
||||||
|
const error = await response.json()
|
||||||
|
message.value = error.detail || 'Erreur lors du rechargement'
|
||||||
|
messageType.value = 'error'
|
||||||
|
messageClass.value = 'bg-monokai-pink/20 text-monokai-pink border border-monokai-pink'
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur rechargement config:', error)
|
||||||
|
message.value = 'Erreur de connexion au serveur'
|
||||||
|
messageType.value = 'error'
|
||||||
|
messageClass.value = 'bg-monokai-pink/20 text-monokai-pink border border-monokai-pink'
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
setTimeout(() => {
|
||||||
|
message.value = ''
|
||||||
|
}, 5000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function importIpscan(event) {
|
||||||
|
const file = event.target.files[0]
|
||||||
|
if (!file) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
message.value = ''
|
||||||
|
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/ips/import/ipscan', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json()
|
||||||
|
message.value = `Import réussi: ${data.imported} nouvelles IPs, ${data.updated} mises à jour`
|
||||||
|
if (data.errors && data.errors.length > 0) {
|
||||||
|
message.value += `\nErreurs: ${data.errors.join(', ')}`
|
||||||
|
}
|
||||||
|
messageType.value = 'success'
|
||||||
|
messageClass.value = 'bg-monokai-green/20 text-monokai-green border border-monokai-green'
|
||||||
|
emit('configReloaded') // Rafraîchir la liste des IPs
|
||||||
|
} else {
|
||||||
|
const error = await response.json()
|
||||||
|
message.value = error.detail || 'Erreur lors de l\'import'
|
||||||
|
messageType.value = 'error'
|
||||||
|
messageClass.value = 'bg-monokai-pink/20 text-monokai-pink border border-monokai-pink'
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur import:', error)
|
||||||
|
message.value = 'Erreur de connexion au serveur'
|
||||||
|
messageType.value = 'error'
|
||||||
|
messageClass.value = 'bg-monokai-pink/20 text-monokai-pink border border-monokai-pink'
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
event.target.value = '' // Reset input
|
||||||
|
setTimeout(() => {
|
||||||
|
message.value = ''
|
||||||
|
}, 8000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -2,6 +2,7 @@ import { createApp } from 'vue'
|
|||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import './assets/main.css'
|
import './assets/main.css'
|
||||||
|
import '@mdi/font/css/materialdesignicons.css'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
const pinia = createPinia()
|
const pinia = createPinia()
|
||||||
|
|||||||
@@ -18,6 +18,16 @@ export const useIPStore = defineStore('ip', () => {
|
|||||||
known: 0,
|
known: 0,
|
||||||
unknown: 0
|
unknown: 0
|
||||||
})
|
})
|
||||||
|
const lastScanDate = ref(null)
|
||||||
|
const scanProgress = ref({
|
||||||
|
current: 0,
|
||||||
|
total: 0,
|
||||||
|
currentIP: null
|
||||||
|
})
|
||||||
|
const isScanning = ref(false)
|
||||||
|
const uiConfig = ref({
|
||||||
|
cell_size: 30
|
||||||
|
})
|
||||||
|
|
||||||
// Filtres
|
// Filtres
|
||||||
const filters = ref({
|
const filters = ref({
|
||||||
@@ -51,6 +61,36 @@ export const useIPStore = defineStore('ip', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
|
async function fetchUIConfig() {
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/api/config/ui')
|
||||||
|
uiConfig.value = response.data
|
||||||
|
// Appliquer la taille des cellules, de la police et de l'espacement via variables CSS
|
||||||
|
document.documentElement.style.setProperty('--cell-size', `${response.data.cell_size}px`)
|
||||||
|
document.documentElement.style.setProperty('--font-size', `${response.data.font_size}px`)
|
||||||
|
document.documentElement.style.setProperty('--cell-gap', `${response.data.cell_gap}px`)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Erreur chargement config UI:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reloadConfig() {
|
||||||
|
try {
|
||||||
|
const response = await axios.post('/api/config/reload')
|
||||||
|
if (response.data.success) {
|
||||||
|
// Appliquer la nouvelle config UI
|
||||||
|
uiConfig.value = response.data.ui
|
||||||
|
document.documentElement.style.setProperty('--cell-size', `${response.data.ui.cell_size}px`)
|
||||||
|
document.documentElement.style.setProperty('--font-size', `${response.data.ui.font_size}px`)
|
||||||
|
document.documentElement.style.setProperty('--cell-gap', `${response.data.ui.cell_gap}px`)
|
||||||
|
return response.data.message
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Erreur rechargement config:', err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchIPs() {
|
async function fetchIPs() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
@@ -97,6 +137,27 @@ export const useIPStore = defineStore('ip', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteIP(ipAddress) {
|
||||||
|
try {
|
||||||
|
await axios.delete(`/api/ips/${ipAddress}`)
|
||||||
|
|
||||||
|
// Retirer du store
|
||||||
|
const index = ips.value.findIndex(ip => ip.ip === ipAddress)
|
||||||
|
if (index !== -1) {
|
||||||
|
ips.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedIP.value?.ip === ipAddress) {
|
||||||
|
selectedIP.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
await fetchStats()
|
||||||
|
} catch (err) {
|
||||||
|
error.value = err.message
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function getIPHistory(ipAddress, hours = 24) {
|
async function getIPHistory(ipAddress, hours = 24) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`/api/ips/${ipAddress}/history?hours=${hours}`)
|
const response = await axios.get(`/api/ips/${ipAddress}/history?hours=${hours}`)
|
||||||
@@ -172,12 +233,28 @@ export const useIPStore = defineStore('ip', () => {
|
|||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case 'scan_start':
|
case 'scan_start':
|
||||||
// Notification début de scan
|
// Notification début de scan
|
||||||
|
isScanning.value = true
|
||||||
|
scanProgress.value = {
|
||||||
|
current: 0,
|
||||||
|
total: message.total || 0,
|
||||||
|
currentIP: null
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'scan_progress':
|
||||||
|
// Progression du scan
|
||||||
|
if (message.current) scanProgress.value.current = message.current
|
||||||
|
if (message.total) scanProgress.value.total = message.total
|
||||||
|
if (message.ip) scanProgress.value.currentIP = message.ip
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'scan_complete':
|
case 'scan_complete':
|
||||||
// Rafraîchir les données après scan
|
// Rafraîchir les données après scan
|
||||||
|
isScanning.value = false
|
||||||
|
lastScanDate.value = new Date()
|
||||||
|
scanProgress.value = { current: 0, total: 0, currentIP: null }
|
||||||
fetchIPs()
|
fetchIPs()
|
||||||
stats.value = message.stats
|
if (message.stats) stats.value = message.stats
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'ip_update':
|
case 'ip_update':
|
||||||
@@ -212,14 +289,21 @@ export const useIPStore = defineStore('ip', () => {
|
|||||||
stats,
|
stats,
|
||||||
filters,
|
filters,
|
||||||
wsConnected,
|
wsConnected,
|
||||||
|
lastScanDate,
|
||||||
|
scanProgress,
|
||||||
|
isScanning,
|
||||||
|
uiConfig,
|
||||||
|
|
||||||
// Computed
|
// Computed
|
||||||
filteredIPs,
|
filteredIPs,
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
|
fetchUIConfig,
|
||||||
|
reloadConfig,
|
||||||
fetchIPs,
|
fetchIPs,
|
||||||
fetchStats,
|
fetchStats,
|
||||||
updateIP,
|
updateIP,
|
||||||
|
deleteIP,
|
||||||
getIPHistory,
|
getIPHistory,
|
||||||
startScan,
|
startScan,
|
||||||
selectIP,
|
selectIP,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export default {
|
|||||||
pink: '#F92672',
|
pink: '#F92672',
|
||||||
cyan: '#66D9EF',
|
cyan: '#66D9EF',
|
||||||
purple: '#AE81FF',
|
purple: '#AE81FF',
|
||||||
|
'purple-dark': '#5E4B8C', // Violet foncé pour détections anciennes
|
||||||
yellow: '#E6DB74',
|
yellow: '#E6DB74',
|
||||||
orange: '#FD971F',
|
orange: '#FD971F',
|
||||||
},
|
},
|
||||||
|
|||||||
940
ipscan.xml
Normal file
940
ipscan.xml
Normal file
@@ -0,0 +1,940 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<!-- This file has been generated by Angry IP Scanner 3.9.2 -->
|
||||||
|
<!-- Visit the website at https://angryip.org -->
|
||||||
|
<scanning_report time="2025-12-06 08:04:28">
|
||||||
|
<feeder><![CDATA[10.0.0.0 - 10.0.3.255]]></feeder>
|
||||||
|
<hosts>
|
||||||
|
<host address="10.0.0.2">
|
||||||
|
<result name="IP"><![CDATA[10.0.0.2]]></result>
|
||||||
|
<result name="Ping"><![CDATA[0 ms]]></result>
|
||||||
|
<result name="Nom d'hôte"><![CDATA[homeassistant.local]]></result>
|
||||||
|
<result name="Ports"><![CDATA[22,8123,9000]]></result>
|
||||||
|
<result name="Constructeur MAC"><![CDATA[[n/a]]]></result>
|
||||||
|
<result name="Adresse MAC"><![CDATA[02:E2:88:D4:02:62]]></result>
|
||||||
|
<result name="Détection Web"><![CDATA[[n/a]]]></result>
|
||||||
|
<result name="Info NetBIOS"><![CDATA[HOME\HOMEASSISTANT@HOMEASSISTANT [00-00-00-00-00-00]]]></result>
|
||||||
|
</host>
|
||||||
|
<host address="10.0.0.5">
|
||||||
|
<result name="IP"><![CDATA[10.0.0.5]]></result>
|
||||||
|
<result name="Ping"><![CDATA[0 ms]]></result>
|
||||||
|
<result name="Nom d'hôte"><![CDATA[debian-vm5.local]]></result>
|
||||||
|
<result name="Ports"><![CDATA[22,8080]]></result>
|
||||||
|
<result name="Constructeur MAC"><![CDATA[Proxmox Server]]></result>
|
||||||
|
<result name="Adresse MAC"><![CDATA[BC:24:11:2C:D8:8F]]></result>
|
||||||
|
<result name="Détection Web"><![CDATA[[n/a]]]></result>
|
||||||
|
<result name="Info NetBIOS"><![CDATA[HOME\DEBIAN-VM5@DEBIAN-VM5 [00-00-00-00-00-00]]]></result>
|
||||||
|
</host>
|
||||||
|
<host address="10.0.0.3">
|
||||||
|
<result name="IP"><![CDATA[10.0.0.3]]></result>
|
||||||
|
<result name="Ping"><![CDATA[0 ms]]></result>
|
||||||
|
<result name="Nom d'hôte"><![CDATA[vm-mqtt.local]]></result>
|
||||||
|
<result name="Ports"><![CDATA[22]]></result>
|
||||||
|
<result name="Constructeur MAC"><![CDATA[[n/a]]]></result>
|
||||||
|
<result name="Adresse MAC"><![CDATA[92:A5:9D:DC:FF:A6]]></result>
|
||||||
|
<result name="Détection Web"><![CDATA[[n/a]]]></result>
|
||||||
|
<result name="Info NetBIOS"><![CDATA[[n/a]]]></result>
|
||||||
|
</host>
|
||||||
|
<host address="10.0.0.8">
|
||||||
|
<result name="IP"><![CDATA[10.0.0.8]]></result>
|
||||||
|
<result name="Ping"><![CDATA[0 ms]]></result>
|
||||||
|
<result name="Nom d'hôte"><![CDATA[vm-docker8.local]]></result>
|
||||||
|
<result name="Ports"><![CDATA[22]]></result>
|
||||||
|
<result name="Constructeur MAC"><![CDATA[[n/a]]]></result>
|
||||||
|
<result name="Adresse MAC"><![CDATA[26:F9:1C:CD:2E:68]]></result>
|
||||||
|
<result name="Détection Web"><![CDATA[[n/a]]]></result>
|
||||||
|
<result name="Info NetBIOS"><![CDATA[HOME\VM-DOCKER8@VM-DOCKER8 [00-00-00-00-00-00]]]></result>
|
||||||
|
</host>
|
||||||
|
<host address="10.0.0.4">
|
||||||
|
<result name="IP"><![CDATA[10.0.0.4]]></result>
|
||||||
|
<result name="Ping"><![CDATA[0 ms]]></result>
|
||||||
|
<result name="Nom d'hôte"><![CDATA[domotic.local]]></result>
|
||||||
|
<result name="Ports"><![CDATA[22,8006]]></result>
|
||||||
|
<result name="Constructeur MAC"><![CDATA[Hewlett Packard]]></result>
|
||||||
|
<result name="Adresse MAC"><![CDATA[EC:B1:D7:69:21:B7]]></result>
|
||||||
|
<result name="Détection Web"><![CDATA[[n/a]]]></result>
|
||||||
|
<result name="Info NetBIOS"><![CDATA[[n/a]]]></result>
|
||||||
|
</host>
|
||||||
|
<host address="10.0.0.9">
|
||||||
|
<result name="IP"><![CDATA[10.0.0.9]]></result>
|
||||||
|
<result name="Ping"><![CDATA[0 ms]]></result>
|
||||||
|
<result name="Nom d'hôte"><![CDATA[vm-nodered.local]]></result>
|
||||||
|
<result name="Ports"><![CDATA[22]]></result>
|
||||||
|
<result name="Constructeur MAC"><![CDATA[Proxmox Server]]></result>
|
||||||
|
<result name="Adresse MAC"><![CDATA[BC:24:11:37:07:A1]]></result>
|
||||||
|
<result name="Détection Web"><![CDATA[[n/a]]]></result>
|
||||||
|
<result name="Info NetBIOS"><![CDATA[[n/a]]]></result>
|
||||||
|
</host>
|
||||||
|
<host address="10.0.0.10">
|
||||||
|
<result name="IP"><![CDATA[10.0.0.10]]></result>
|
||||||
|
<result name="Ping"><![CDATA[0 ms]]></result>
|
||||||
|
<result name="Nom d'hôte"><![CDATA[serv-dns.local]]></result>
|
||||||
|
<result name="Ports"><![CDATA[22]]></result>
|
||||||
|
<result name="Constructeur MAC"><![CDATA[[n/a]]]></result>
|
||||||
|
<result name="Adresse MAC"><![CDATA[7E:A7:24:20:4C:1F]]></result>
|
||||||
|
<result name="Détection Web"><![CDATA[[n/a]]]></result>
|
||||||
|
<result name="Info NetBIOS"><![CDATA[[n/a]]]></result>
|
||||||
|
</host>
|
||||||
|
<host address="10.0.0.13">
|
||||||
|
<result name="IP"><![CDATA[10.0.0.13]]></result>
|
||||||
|
<result name="Ping"><![CDATA[0 ms]]></result>
|
||||||
|
<result name="Nom d'hôte"><![CDATA[VM-DETECT-CAM]]></result>
|
||||||
|
<result name="Ports"><![CDATA[22]]></result>
|
||||||
|
<result name="Constructeur MAC"><![CDATA[Proxmox Server]]></result>
|
||||||
|
<result name="Adresse MAC"><![CDATA[BC:24:11:FE:8C:BA]]></result>
|
||||||
|
<result name="Détection Web"><![CDATA[[n/a]]]></result>
|
||||||
|
<result name="Info NetBIOS"><![CDATA[HOME\VM-DETECT-CAM@VM-DETECT-CAM [00-00-00-00-00-00]]]></result>
|
||||||
|
</host>
|
||||||
|
<host address="10.0.0.15">
|
||||||
|
<result name="IP"><![CDATA[10.0.0.15]]></result>
|
||||||
|
<result name="Ping"><![CDATA[0 ms]]></result>
|
||||||
|
<result name="Nom d'hôte"><![CDATA[adguard.home]]></result>
|
||||||
|
<result name="Ports"><![CDATA[22,80]]></result>
|
||||||
|
<result name="Constructeur MAC"><![CDATA[Proxmox Server]]></result>
|
||||||
|
<result name="Adresse MAC"><![CDATA[BC:24:11:ED:2B:2B]]></result>
|
||||||
|
<result name="Détection Web"><![CDATA[[n/a]]]></result>
|
||||||
|
<result name="Info NetBIOS"><![CDATA[[n/a]]]></result>
|
||||||
|
</host>
|
||||||
|
<host address="10.0.0.6">
|
||||||
|
<result name="IP"><![CDATA[10.0.0.6]]></result>
|
||||||
|
<result name="Ping"><![CDATA[28 ms]]></result>
|
||||||
|
<result name="Nom d'hôte"><![CDATA[pizeroComble.local]]></result>
|
||||||
|
<result name="Ports"><![CDATA[22,80]]></result>
|
||||||
|
<result name="Constructeur MAC"><![CDATA[Raspberry Pi Foundation]]></result>
|
||||||
|
<result name="Adresse MAC"><![CDATA[B8:27:EB:7C:6D:F4]]></result>
|
||||||
|
<result name="Détection Web"><![CDATA[Apache/2.4.25 (Raspbian)]]></result>
|
||||||
|
<result name="Info NetBIOS"><![CDATA[HOME\PIZEROCOMBLE@PIZEROCOMBLE [00-00-00-00-00-00]]]></result>
|
||||||
|
</host>
|
||||||
|
<host address="10.0.0.26">
|
||||||
|
<result name="IP"><![CDATA[10.0.0.26]]></result>
|
||||||
|
<result name="Ping"><![CDATA[0 ms]]></result>
|
||||||
|
<result name="Nom d'hôte"><![CDATA[vm-docker26.local]]></result>
|
||||||
|
<result name="Ports"><![CDATA[22,8006]]></result>
|
||||||
|
<result name="Constructeur MAC"><![CDATA[[n/a]]]></result>
|
||||||
|
<result name="Adresse MAC"><![CDATA[8E:B4:AB:2A:BA:7B]]></result>
|
||||||
|
<result name="Détection Web"><![CDATA[[n/a]]]></result>
|
||||||
|
<result name="Info NetBIOS"><![CDATA[HOME\VM-DOCKER26@VM-DOCKER26 [00-00-00-00-00-00]]]></result>
|
||||||
|
</host>
|
||||||
|
<host address="10.0.0.17">
|
||||||
|
<result name="IP"><![CDATA[10.0.0.17]]></result>
|
||||||
|
<result name="Ping"><![CDATA[0 ms]]></result>
|
||||||
|
<result name="Nom d'hôte"><![CDATA[[n/a]]]></result>
|
||||||
|
<result name="Ports"><![CDATA[22,8080]]></result>
|
||||||
|
<result name="Constructeur MAC"><![CDATA[Proxmox Server]]></result>
|
||||||
|
<result name="Adresse MAC"><![CDATA[BC:24:11:FA:37:3E]]></result>
|
||||||
|
<result name="Détection Web"><![CDATA[[n/a]]]></result>
|
||||||
|
<result name="Info NetBIOS"><![CDATA[[n/a]]]></result>
|
||||||
|
</host>
|
||||||
|
<host address="10.0.0.7">
|
||||||
|
<result name="IP"><![CDATA[10.0.0.7]]></result>
|
||||||
|
<result name="Ping"><![CDATA[3 ms]]></result>
|
||||||
|
<result name="Nom d'hôte"><![CDATA[pichaudiere.local]]></result>
|
||||||
|
<result name="Ports"><![CDATA[[n/a]]]></result>
|
||||||
|
<result name="Constructeur MAC"><![CDATA[Raspberry Pi Foundation]]></result>
|
||||||
|
<result name="Adresse MAC"><![CDATA[B8:27:EB:33:74:41]]></result>
|
||||||
|
<result name="Détection Web"><![CDATA[[n/a]]]></result>
|
||||||
|
<result name="Info NetBIOS"><![CDATA[HOME\PICHAUDIERE@PICHAUDIERE [00-00-00-00-00-00]]]></result>
|
||||||
|
</host>
|
||||||
|
<host address="10.0.0.101">
|
||||||
|
<result name="IP"><![CDATA[10.0.0.101]]></result>
|
||||||
|
<result name="Ping"><![CDATA[0 ms]]></result>
|
||||||
|
<result name="Nom d'hôte"><![CDATA[elitedesk.local]]></result>
|
||||||
|
<result name="Ports"><![CDATA[22,8006]]></result>
|
||||||
|
<result name="Constructeur MAC"><![CDATA[Hewlett Packard]]></result>
|
||||||
|
<result name="Adresse MAC"><![CDATA[18:60:24:B0:80:EC]]></result>
|
||||||
|
<result name="Détection Web"><![CDATA[[n/a]]]></result>
|
||||||
|
<result name="Info NetBIOS"><![CDATA[HOME\ELITEDESK@ELITEDESK [00-00-00-00-00-00]]]></result>
|
||||||
|
</host>
|
||||||
|
<host address="10.0.0.90">
|
||||||
|
<result name="IP"><![CDATA[10.0.0.90]]></result>
|
||||||
|
<result name="Ping"><![CDATA[0 ms]]></result>
|
||||||
|
<result name="Nom d'hôte"><![CDATA[[n/a]]]></result>
|
||||||
|
<result name="Ports"><![CDATA[80]]></result>
|
||||||
|
<result name="Constructeur MAC"><![CDATA[Jinan USR IOT]]></result>
|
||||||
|
<result name="Adresse MAC"><![CDATA[D4:AD:20:3C:BD:A6]]></result>
|
||||||
|
<result name="Détection Web"><![CDATA[[n/a]]]></result>
|
||||||
|
<result name="Info NetBIOS"><![CDATA[[n/a]]]></result>
|
||||||
|
</host>
|
||||||
|
<host address="10.0.0.105">
|
||||||
|
<result name="IP"><![CDATA[10.0.0.105]]></result>
|
||||||
|
<result name="Ping"><![CDATA[0 ms]]></result>
|
||||||
|
<result name="Nom d'hôte"><![CDATA[VM-ZIGBEE2MQTT]]></result>
|
||||||
|
<result name="Ports"><![CDATA[22,8080,9000]]></result>
|
||||||
|
<result name="Constructeur MAC"><![CDATA[Proxmox Server]]></result>
|
||||||
|
<result name="Adresse MAC"><![CDATA[BC:24:11:AF:57:52]]></result>
|
||||||
|
<result name="Détection Web"><![CDATA[[n/a]]]></result>
|
||||||
|
<result name="Info NetBIOS"><![CDATA[HOME\VM-ZIGBEE2MQTT@VM-ZIGBEE2MQTT [00-00-00-00-00-00]]]></result>
|
||||||
|
</host>
|
||||||
|
<host address="10.0.0.106">
|
||||||
|
<result name="IP"><![CDATA[10.0.0.106]]></result>
|
||||||
|
<result name="Ping"><![CDATA[0 ms]]></result>
|
||||||
|
<result name="Nom d'hôte"><![CDATA[VM-ZIGBEE2MQTT]]></result>
|
||||||
|
<result name="Ports"><![CDATA[22,8080,9000]]></result>
|
||||||
|
<result name="Constructeur MAC"><![CDATA[Proxmox Server]]></result>
|
||||||
|
<result name="Adresse MAC"><![CDATA[BC:24:11:F9:8F:44]]></result>
|
||||||
|
<result name="Détection Web"><![CDATA[[n/a]]]></result>
|
||||||
|
<result name="Info NetBIOS"><![CDATA[HOME\VM-ZIGBEE2MQTT@VM-ZIGBEE2MQTT [00-00-00-00-00-00]]]></result>
|
||||||
|
</host>
|
||||||
|
<host address="10.0.0.116">
|
||||||
|
<result name="IP"><![CDATA[10.0.0.116]]></result>
|
||||||
|
<result name="Ping"><![CDATA[0 ms]]></result>
|
||||||
|
<result name="Nom d'hôte"><![CDATA[VM-DOCKER116]]></result>
|
||||||
|
<result name="Ports"><![CDATA[22,8080]]></result>
|
||||||
|
<result name="Constructeur MAC"><![CDATA[Proxmox Server]]></result>
|
||||||
|
<result name="Adresse MAC"><![CDATA[BC:24:11:5A:27:67]]></result>
|
||||||
|
<result name="Détection Web"><![CDATA[[n/a]]]></result>
|
||||||
|
<result name="Info NetBIOS"><![CDATA[HOME\VM-DOCKER116@VM-DOCKER116 [00-00-00-00-00-00]]]></result>
|
||||||
|
</host>
|
||||||
|
<host address="10.0.0.135">
|
||||||
|
<result name="IP"><![CDATA[10.0.0.135]]></result>
|
||||||
|
<result name="Ping"><![CDATA[0 ms]]></result>
|
||||||
|
<result name="Nom d'hôte"><![CDATA[VM-DOCKER135]]></result>
|
||||||
|
<result name="Ports"><![CDATA[22,8080,9000]]></result>
|
||||||
|
<result name="Constructeur MAC"><![CDATA[Proxmox Server]]></result>
|
||||||
|
<result name="Adresse MAC"><![CDATA[BC:24:11:B9:96:72]]></result>
|
||||||
|
<result name="Détection Web"><![CDATA[[n/a]]]></result>
|
||||||
|
<result name="Info NetBIOS"><![CDATA[HOME\VM-DOCKER135@VM-DOCKER135 [00-00-00-00-00-00]]]></result>
|
||||||
|
</host>
|
||||||
|
<host address="10.0.0.201">
|
||||||
|
<result name="IP"><![CDATA[10.0.0.201]]></result>
|
||||||
|
<result name="Ping"><![CDATA[0 ms]]></result>
|
||||||
|
<result name="Nom d'hôte"><![CDATA[vm-backup-pve.local]]></result>
|
||||||
|
<result name="Ports"><![CDATA[22]]></result>
|
||||||
|
<result name="Constructeur MAC"><![CDATA[Proxmox Server]]></result>
|
||||||
|
<result name="Adresse MAC"><![CDATA[BC:24:11:E0:A5:E0]]></result>
|
||||||
|
<result name="Détection Web"><![CDATA[[n/a]]]></result>
|
||||||
|
<result name="Info NetBIOS"><![CDATA[HOME\VM-BACKUP-PVE@VM-BACKUP-PVE [00-00-00-00-00-00]]]></result>
|
||||||
|
</host>
|
||||||
|
<host address="10.0.0.202">
|
||||||
|
<result name="IP"><![CDATA[10.0.0.202]]></result>
|
||||||
|
<result name="Ping"><![CDATA[0 ms]]></result>
|
||||||
|
<result name="Nom d'hôte"><![CDATA[vm-vaultwarden.local]]></result>
|
||||||
|
<result name="Ports"><![CDATA[22,80,443,8080,9000]]></result>
|
||||||
|
<result name="Constructeur MAC"><![CDATA[Proxmox Server]]></result>
|
||||||
|
<result name="Adresse MAC"><![CDATA[BC:24:11:B3:15:32]]></result>
|
||||||
|
<result name="Détection Web"><![CDATA[[n/a]]]></result>
|
||||||
|
<result name="Info NetBIOS"><![CDATA[HOME\VM-VAULTWARDEN@VM-VAULTWARDEN [00-00-00-00-00-00]]]></result>
|
||||||
|
</host>
|
||||||
|
<host address="10.0.0.203">
|
||||||
|
<result name="IP"><![CDATA[10.0.0.203]]></result>
|
||||||
|
<result name="Ping"><![CDATA[0 ms]]></result>
|
||||||
|
<result name="Nom d'hôte"><![CDATA[VM-HTTP]]></result>
|
||||||
|
<result name="Ports"><![CDATA[22,80]]></result>
|
||||||
|
<result name="Constructeur MAC"><![CDATA[Proxmox Server]]></result>
|
||||||
|
<result name="Adresse MAC"><![CDATA[BC:24:11:89:DF:80]]></result>
|
||||||
|
<result name="Détection Web"><![CDATA[nginx]]></result>
|
||||||
|
<result name="Info NetBIOS"><![CDATA[HOME\VM-HTTP@VM-HTTP [00-00-00-00-00-00]]]></result>
|
||||||
|
</host>
|
||||||
|
<host address="10.0.0.214">
|
||||||
|
<result name="IP"><![CDATA[10.0.0.214]]></result>
|
||||||
|
<result name="Ping"><![CDATA[0 ms]]></result>
|
||||||
|
<result name="Nom d'hôte"><![CDATA[vm-partage.local]]></result>
|
||||||
|
<result name="Ports"><![CDATA[22,80]]></result>
|
||||||
|
<result name="Constructeur MAC"><![CDATA[Proxmox Server]]></result>
|
||||||
|
<result name="Adresse MAC"><![CDATA[BC:24:11:DF:4B:F9]]></result>
|
||||||
|
<result name="Détection Web"><![CDATA[Apache/2.4.65 (Debian)]]></result>
|
||||||
|
<result name="Info NetBIOS"><![CDATA[HOME\DEBIAN-SHARE@DEBIAN-SHARE [00-00-00-00-00-00]]]></result>
|
||||||
|
</host>
|
||||||
|
<host address="10.0.0.186">
|
||||||
|
<result name="IP"><![CDATA[10.0.0.186]]></result>
|
||||||
|
<result name="Ping"><![CDATA[1 ms]]></result>
|
||||||
|
<result name="Nom d'hôte"><![CDATA[n-addrarpa | ||||||