diff --git "a/docs/Images/Image coll\303\251e.png" "b/docs/Images/Image coll\303\251e.png" new file mode 100644 index 0000000..7352491 Binary files /dev/null and "b/docs/Images/Image coll\303\251e.png" differ diff --git a/docs/Images/mockup-final.png b/docs/Images/mockup-final.png new file mode 100644 index 0000000..f57ca06 Binary files /dev/null and b/docs/Images/mockup-final.png differ diff --git a/nbactions.xml b/nbactions.xml new file mode 100644 index 0000000..186cf9c --- /dev/null +++ b/nbactions.xml @@ -0,0 +1,19 @@ + + + + run + + jar + + + javafx:run + + + + ${exec.vmArgs} -classpath %classpath ${exec.mainClass} ${exec.appArgs} + + ${packageClassName} + java + + + diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 873c65e..505ba0e 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -26,7 +26,7 @@ opens tg.cyberlabmanager.controller to javafx.fxml; opens tg.cyberlabmanager.ui to javafx.fxml; - // Exports publics + // ── Exports publics exports tg.cyberlabmanager.app; exports tg.cyberlabmanager.controller; exports tg.cyberlabmanager.model; @@ -34,4 +34,4 @@ exports tg.cyberlabmanager.hypervisor; exports tg.cyberlabmanager.pdf; exports tg.cyberlabmanager.ui; -} +} \ No newline at end of file diff --git a/src/main/java/tg/cyberlabmanager/app/CyberLabApp.java b/src/main/java/tg/cyberlabmanager/app/CyberLabApp.java index d8770e1..0f55392 100644 --- a/src/main/java/tg/cyberlabmanager/app/CyberLabApp.java +++ b/src/main/java/tg/cyberlabmanager/app/CyberLabApp.java @@ -4,13 +4,15 @@ import javafx.fxml.FXMLLoader; import javafx.scene.Scene; import javafx.stage.Stage; - -import java.io.IOException; +import tg.cyberlabmanager.controller.LabController; /** * Application JavaFX principale de Cyber Lab Manager. */ public class CyberLabApp extends Application { + + private LabController controller; + /** * Cree l'application JavaFX. */ @@ -24,11 +26,27 @@ public CyberLabApp() { * @throws IOException si le chargement du fichier FXML echoue */ @Override - public void start(Stage stage) throws IOException { - FXMLLoader loader = new FXMLLoader(CyberLabApp.class.getResource("/tg/cyberlabmanager/ui/main-view.fxml")); - Scene scene = new Scene(loader.load(), 900, 600); + public void start(Stage stage) throws Exception { + FXMLLoader loader = new FXMLLoader( + CyberLabApp.class.getResource("/tg/cyberlabmanager/ui/main-view.fxml") + ); + Scene scene = new Scene(loader.load(), 1400, 820); + controller = loader.getController(); stage.setTitle("Cyber Lab Manager"); + stage.setMinWidth(1100); + stage.setMinHeight(650); stage.setScene(scene); stage.show(); } + + @Override + public void stop() { + if (controller != null) { + controller.shutdown(); + } + } + + public static void main(String[] args) { + launch(args); + } } diff --git a/src/main/java/tg/cyberlabmanager/controller/LabController.java b/src/main/java/tg/cyberlabmanager/controller/LabController.java index bc81b5a..bdc61aa 100644 --- a/src/main/java/tg/cyberlabmanager/controller/LabController.java +++ b/src/main/java/tg/cyberlabmanager/controller/LabController.java @@ -1,174 +1,1143 @@ package tg.cyberlabmanager.controller; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.stage.FileChooser; +import java.io.File; + import tg.cyberlabmanager.data.DatabaseManager; import tg.cyberlabmanager.hypervisor.HypervisorException; import tg.cyberlabmanager.hypervisor.IHypervisor; +import tg.cyberlabmanager.hypervisor.VBoxOrchestrator; import tg.cyberlabmanager.model.AppConfig; +import tg.cyberlabmanager.model.AuditEntry; import tg.cyberlabmanager.model.Lab; import tg.cyberlabmanager.model.VirtualMachine; +import tg.cyberlabmanager.model.Snapshot; +import tg.cyberlabmanager.model.VMDescriptor; import tg.cyberlabmanager.pdf.PdfExporter; -import javafx.fxml.FXML; -import javafx.scene.control.Label; +import tg.cyberlabmanager.model.VMStatus; +import tg.cyberlabmanager.ui.LabListView; +import tg.cyberlabmanager.ui.VmTableView; +import tg.cyberlabmanager.model.JournalEntry; +import java.time.LocalDateTime; import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Consumer; +import java.util.ArrayList; +import static tg.cyberlabmanager.model.VMStatus.PAUSED; +import static tg.cyberlabmanager.model.VMStatus.POWERED_OFF; +import static tg.cyberlabmanager.model.VMStatus.RUNNING; +import static tg.cyberlabmanager.model.VMStatus.SAVED; /** - * Contrôleur principal coordonnant l'interface, la persistance et l'hyperviseur. + * Contrôleur principal responsable de l'affichage de l'interface (Rôle 1). + * + *

+ * Coordonne l'UI, la persistance ({@link DatabaseManager}, Rôle 2) et le + * pilotage de l'hyperviseur VirtualBox ({@link VBoxOrchestrator}, Rôle 3). + *

*/ public class LabController { + private IHypervisor hypervisor; private DatabaseManager databaseManager; private PdfExporter pdfExporter; + private Lab selectedLab; + private VirtualMachine selectedVM; + private boolean auditExpanded = false; + private boolean viewingOrphans = false; + + private final ExecutorService executor = Executors.newCachedThreadPool(); @FXML private Label statusLabel; + @FXML + private javafx.scene.layout.VBox labListContainer; + @FXML + private Label labTitleLabel; + @FXML + private javafx.scene.control.Button btnExportPdf; + @FXML + private javafx.scene.layout.HBox statsBar; + @FXML + private Label statTotal; + @FXML + private Label statRunning; + @FXML + private Label statSnapshots; + @FXML + private javafx.scene.layout.HBox vmTableHeader; + @FXML + private javafx.scene.layout.VBox vmListContainer; + @FXML + private Label vmEmptyLabel; + @FXML + private Label selectedVmNameLabel; + @FXML + private javafx.scene.control.Button btnStart; + @FXML + private javafx.scene.control.Button btnStop; + @FXML + private javafx.scene.control.Button btnSaveState; + @FXML + private javafx.scene.control.Button btnTakeSnapshot; + @FXML + private javafx.scene.control.Button btnAssignLab; + @FXML + private Label controlsPlaceholder; + @FXML + private javafx.scene.control.ComboBox snapshotCombo; + @FXML + private javafx.scene.control.TextArea journalArea; + @FXML + private javafx.scene.layout.VBox snapshotSection; + @FXML + private javafx.scene.control.TextField journalInputField; + @FXML + private javafx.scene.control.TextArea auditArea; + @FXML + private Label auditArrow; + + @FXML + private void initialize() { + this.databaseManager = new DatabaseManager(); + AppConfig config = databaseManager.getConfig(); + + VBoxOrchestrator orchestrator = new VBoxOrchestrator(config); + this.hypervisor = orchestrator; + + if (!orchestrator.isReady()) { + showError("VirtualBox introuvable", + "VBoxManage n'a pas été détecté automatiquement. " + + "Vous pourrez configurer son chemin dans les paramètres."); + } + + this.pdfExporter = new PdfExporter(); + + refreshLabList(); + } + + // ── Affichage de la liste des labos ────────────────────────────────── + private void refreshLabList() { + labListContainer.getChildren().clear(); + + List labs = databaseManager.getAllLabs(); + + for (Lab lab : labs) { + boolean isSelected = selectedLab != null && selectedLab.getId() == lab.getId() && !viewingOrphans; + + String badgeStyle = switch (lab.getCategory().toLowerCase()) { + case "malware" -> + "badge-malware"; + case "pentest" -> + "badge-pentest"; + case "forensics" -> + "badge-forensics"; + case "network" -> + "badge-network"; + default -> + "badge-default"; + }; + + javafx.scene.layout.VBox card = LabListView.createLabCard( + lab.getTitle(), lab.getCategory(), badgeStyle, isSelected); + + card.setOnMouseClicked(e -> onLabCardClicked(lab)); + + labListContainer.getChildren().add(card); + } + + // ── Carte « VMs sans labo » ────────────────────────────────────── + List orphans = databaseManager.getOrphanVMs(); + if (!orphans.isEmpty()) { + javafx.scene.layout.VBox orphanCard = LabListView.createOrphanCard( + orphans.size(), viewingOrphans); + orphanCard.setOnMouseClicked(e -> onOrphanCardClicked(orphans)); + labListContainer.getChildren().add(orphanCard); + } + } /** - * Cree un contrôleur vide utilise par JavaFX lors du chargement FXML. + * Constructeur par défaut initialisant le contrôleur du laboratoire. */ public LabController() { } - /** - * Cree un contrôleur avec ses dépendances injectées. - * - * @param hypervisor service de pilotage de l'hyperviseur - * @param databaseManager service de persistance - * @param pdfExporter service d'export PDF - */ + public void setDependencies(IHypervisor hypervisor, + DatabaseManager databaseManager, + PdfExporter pdfExporter) { + this.hypervisor = hypervisor; + this.databaseManager = databaseManager; + this.pdfExporter = pdfExporter; + } + public LabController(IHypervisor hypervisor, DatabaseManager databaseManager, PdfExporter pdfExporter) { this.hypervisor = hypervisor; this.databaseManager = databaseManager; this.pdfExporter = pdfExporter; } - @FXML - private void initialize() { - if (statusLabel != null) { - statusLabel.setText("Cyber Lab Manager pret"); - } + public void initView() { } - /** - * Déclenche le démarrage d'une machine virtuelle. - * - * @param vm machine virtuelle cible - */ - public void onStartVMClicked(VirtualMachine vm) { - throw new UnsupportedOperationException("Not implemented yet"); + private List getVMsForLab(int labId) { + return databaseManager.getVMsForLab(labId); } - /** - * Déclenche l'arrêt d'une machine virtuelle. - * - * @param vm machine virtuelle cible - */ - public void onStopVMClicked(VirtualMachine vm) { - throw new UnsupportedOperationException("Not implemented yet"); + public List getAllLabs() { + return databaseManager.getAllLabs(); } - /** - * Déclenche la sauvegarde d'etat d'une machine virtuelle. - * - * @param vm machine virtuelle cible - */ - public void onSaveStateClicked(VirtualMachine vm) { - throw new UnsupportedOperationException("Not implemented yet"); + public List getOrphanVMs() { + return databaseManager.getOrphanVMs(); } - /** - * Déclenche la creation d'un snapshot pour une machine virtuelle. - * - * @param vm machine virtuelle cible - * @param name nom du snapshot - * @param description description du snapshot - */ - public void onTakeSnapshotClicked(VirtualMachine vm, String name, String description) { - throw new UnsupportedOperationException("Not implemented yet"); + public void onStartVMClicked(VirtualMachine vm, + Runnable onSuccess, + Consumer onError) { + + executor.submit(() -> { + try { + hypervisor.startVM(vm.getUuid()); + javafx.application.Platform.runLater(() -> { + vm.setStatus(VMStatus.RUNNING); + onSuccess.run(); + }); + } catch (Exception e) { + try { + VMStatus liveStatus = hypervisor.getStatus(vm.getUuid()); + if (liveStatus == VMStatus.RUNNING) { + javafx.application.Platform.runLater(() -> { + vm.setStatus(VMStatus.RUNNING); + onSuccess.run(); + }); + return; + } + } catch (Exception ignored) { + } + javafx.application.Platform.runLater(() -> onError.accept(e.getMessage())); + } + }); } - /** - * Déclenche la restauration d'un snapshot pour une machine virtuelle. - * - * @param vm machine virtuelle cible - * @param snapshotName nom ou identifiant du snapshot - */ - public void onRestoreSnapshotClicked(VirtualMachine vm, String snapshotName) { - throw new UnsupportedOperationException("Not implemented yet"); + public void onStopVMClicked(VirtualMachine vm, + Runnable onSuccess, + Consumer onError) { + + executor.submit(() -> { + try { + hypervisor.stopVM(vm.getUuid()); + javafx.application.Platform.runLater(() -> { + vm.setStatus(VMStatus.POWERED_OFF); + onSuccess.run(); + }); + } catch (Exception e) { + javafx.application.Platform.runLater(() -> onError.accept(e.getMessage())); + } + }); + } + + public void onSaveStateClicked(VirtualMachine vm, + Runnable onSuccess, + Consumer onError) { + + executor.submit(() -> { + try { + hypervisor.saveState(vm.getUuid()); + javafx.application.Platform.runLater(() -> { + vm.setStatus(VMStatus.SAVED); + onSuccess.run(); + }); + } catch (Exception e) { + javafx.application.Platform.runLater(() -> onError.accept(e.getMessage())); + } + }); + } + + public void onTakeSnapshotClicked(VirtualMachine vm, + String name, + String description, + Runnable onSuccess, + Consumer onError) { + + executor.submit(() -> { + try { + hypervisor.takeSnapshot(vm.getUuid(), name, description); + javafx.application.Platform.runLater(onSuccess); + } catch (Exception e) { + javafx.application.Platform.runLater(() -> onError.accept(e.getMessage())); + } + }); + } + + public void onRestoreSnapshotClicked(VirtualMachine vm, + String snapshotName, + Runnable onSuccess, + Consumer onError) { + + executor.submit(() -> { + try { + hypervisor.restoreSnapshot(vm.getUuid(), snapshotName); + javafx.application.Platform.runLater(onSuccess); + } catch (Exception e) { + javafx.application.Platform.runLater(() -> onError.accept(e.getMessage())); + } + }); } - /** - * Ajoute une note d'analyse a une machine virtuelle. - * - * @param vm machine virtuelle cible - * @param text contenu de la note - */ public void onAddJournalEntry(VirtualMachine vm, String text) { - throw new UnsupportedOperationException("Not implemented yet"); + if (vm == null || text == null || text.isBlank()) { + return; + } + JournalEntry entry = new JournalEntry(); + entry.setContent(text); + entry.setTimestamp(LocalDateTime.now()); + databaseManager.addJournalEntry(entry, vm.getId()); } + // ── Import de VM déjà présentes dans VirtualBox ────────────────────── + /** - * Declenche l'import des machines virtuelles disponibles. + * Déclenche le listage des machines virtuelles disponibles dans + * l'hyperviseur, en vue de leur import (rattachement en base). */ public void onImportVMsClicked() { - throw new UnsupportedOperationException("Not implemented yet"); + executor.submit(() -> { + try { + List descriptors = hypervisor.listVMs(); + javafx.application.Platform.runLater(() -> showImportSelectionDialog(descriptors)); + } catch (HypervisorException e) { + javafx.application.Platform.runLater(() -> showError("Erreur listage VM", e.getMessage())); + } + }); + } + + private void showImportSelectionDialog(List descriptors) { + if (descriptors.isEmpty()) { + showError("Aucune VM trouvée", "Aucune machine virtuelle détectée dans VirtualBox."); + return; + } + + // Exclure les VM dont l'UUID est déjà connu en base (tous labos + orphelines + // confondus) + List knownUuids = new ArrayList<>(); + for (Lab lab : databaseManager.getAllLabs()) { + for (VirtualMachine vm : databaseManager.getVMsForLab(lab.getId())) { + knownUuids.add(vm.getUuid()); + } + } + for (VirtualMachine vm : databaseManager.getOrphanVMs()) { + knownUuids.add(vm.getUuid()); + } + + List importable = descriptors.stream() + .filter(d -> !knownUuids.contains(d.uuid())) + .toList(); + + if (importable.isEmpty()) { + showError("Aucune VM à importer", + "Toutes les VM VirtualBox détectées sont déjà rattachées dans l'application."); + return; + } + + javafx.scene.control.ListView listView = new javafx.scene.control.ListView<>(); + listView.getItems().addAll(importable); + listView.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE); + listView.setCellFactory(lv -> new javafx.scene.control.ListCell<>() { + @Override + protected void updateItem(VMDescriptor item, boolean empty) { + super.updateItem(item, empty); + setText(empty || item == null ? null : item.name() + " (" + item.uuid() + ")"); + } + }); + + javafx.scene.control.Dialog> dialog = new javafx.scene.control.Dialog<>(); + dialog.setTitle("Importer des VM"); + dialog.setHeaderText("Sélectionnez les VM à rattacher" + + (selectedLab != null ? " au labo \"" + selectedLab.getTitle() + "\"" : " (sans labo)")); + dialog.getDialogPane().setContent(listView); + dialog.getDialogPane().getButtonTypes().addAll( + javafx.scene.control.ButtonType.OK, javafx.scene.control.ButtonType.CANCEL); + + dialog.setResultConverter(button -> button == javafx.scene.control.ButtonType.OK + ? new ArrayList<>(listView.getSelectionModel().getSelectedItems()) + : null); + + dialog.showAndWait().ifPresent(selectedDescriptors -> { + if (selectedDescriptors.isEmpty()) { + return; + } + + List selectedVMs = new ArrayList<>(); + for (VMDescriptor d : selectedDescriptors) { + VirtualMachine vm = new VirtualMachine(); + vm.setName(d.name()); + vm.setUuid(d.uuid()); + vm.setStatus(VMStatus.UNKNOWN); + selectedVMs.add(vm); + } + + Integer labId = (selectedLab != null) ? selectedLab.getId() : null; + onImportConfirm(selectedVMs, labId); + }); } /** - * Confirme l'import de machines virtuelles selectionnees. + * Persiste en base les machines virtuelles sélectionnées pour import, + * rattachées (ou non) au laboratoire indiqué. * - * @param selectedVMs machines virtuelles selectionnees - * @param labId identifiant du laboratoire cible, ou {@code null} + * @param selectedVMs machines virtuelles sélectionnées + * @param labId identifiant du laboratoire cible, ou {@code null} */ public void onImportConfirm(List selectedVMs, Integer labId) { - throw new UnsupportedOperationException("Not implemented yet"); + executor.submit(() -> { + List alreadyImported = new ArrayList<>(); + List importedOk = new ArrayList<>(); + + for (VirtualMachine vm : selectedVMs) { + try { + databaseManager.saveVirtualMachine(vm, labId); + importedOk.add(vm); + } catch (RuntimeException e) { + // Filet de sécurité : la VM a pu être importée entre le listage et la + // confirmation + if (e.getMessage() != null && e.getMessage().contains("UNIQUE constraint")) { + alreadyImported.add(vm.getName()); + } else { + javafx.application.Platform.runLater(() -> showError("Erreur import VM", e.getMessage())); + return; + } + } + } + + javafx.application.Platform.runLater(() -> { + if (labId != null && selectedLab != null && labId.equals(selectedLab.getId())) { + refreshVmTable(getVMsForLab(selectedLab.getId())); + } + if (!importedOk.isEmpty()) { + appendJournalLine(importedOk.size() + " VM importée(s)"); + persistAuditEntry(null, "Import VM", + importedOk.size() + " VM importée(s)" + + (labId == null ? " (orphelines)" : ""), + selectedLab != null ? selectedLab.getTitle() : null); + } + if (!alreadyImported.isEmpty()) { + showError("VM déjà importées", + "Ces VM étaient déjà connues et n'ont pas été réimportées : " + + String.join(", ", alreadyImported)); + } + }); + }); + } + + public void onExportLabClicked(Lab lab, String filePath) { + executor.submit(() -> { + try { + List vms = databaseManager.getVMsForLab(lab.getId()); + + List allSnapshots = new ArrayList<>(); + for (VirtualMachine vm : vms) { + allSnapshots.addAll(databaseManager.getSnapshotsForVM(vm.getId())); + } + + List allJournals = new ArrayList<>(); + for (VirtualMachine vm : vms) { + allJournals.addAll(databaseManager.getJournalEntriesForVM(vm.getId())); + } + + pdfExporter.exportLabToPdf(lab, vms, allSnapshots, allJournals, filePath); + + javafx.application.Platform.runLater(() -> { + appendJournalLine("✅ Export PDF terminé : " + filePath); + persistAuditEntry(null, "Export PDF", + "Export PDF du labo \"" + lab.getTitle() + "\"", lab.getTitle()); + }); + + } catch (Exception e) { + javafx.application.Platform.runLater(() -> { + appendJournalLine("❌ Erreur export PDF : " + e.getMessage()); + showError("Erreur export PDF", e.getMessage()); + }); + } + }); + } + + public void onExportOrphansClicked(String filePath) { } /** - * Declenche l'export PDF d'un laboratoire. + * Persiste la configuration mise à jour dans la base et l'applique à + * l'hyperviseur déjà instancié. * - * @param lab laboratoire a exporter + * @param newConfig nouvelle configuration applicative */ - public void onExportLabClicked(Lab lab) { - throw new UnsupportedOperationException("Not implemented yet"); + public void onConfigUpdated(AppConfig newConfig) { + try { + hypervisor.applyConfig(newConfig); + databaseManager.saveConfig(newConfig); + appendJournalLine("Configuration mise à jour"); + } catch (HypervisorException e) { + showError("Erreur configuration hyperviseur", e.getMessage()); + } } - /** - * Declenche l'export des machines virtuelles sans laboratoire. - */ - public void onExportOrphansClicked() { - throw new UnsupportedOperationException("Not implemented yet"); + @FXML + private void handleNewLab() { + javafx.scene.control.Dialog dialog = new javafx.scene.control.Dialog<>(); + dialog.setTitle("Nouveau laboratoire"); + dialog.setHeaderText("Créer un nouveau laboratoire"); + + javafx.scene.control.ButtonType createButtonType = new javafx.scene.control.ButtonType("Créer", + javafx.scene.control.ButtonBar.ButtonData.OK_DONE); + dialog.getDialogPane().getButtonTypes().addAll( + createButtonType, javafx.scene.control.ButtonType.CANCEL); + + javafx.scene.control.TextField titleField = new javafx.scene.control.TextField(); + titleField.setPromptText("Titre du labo"); + + javafx.scene.control.TextArea descriptionField = new javafx.scene.control.TextArea(); + descriptionField.setPromptText("Description"); + descriptionField.setPrefRowCount(3); + + javafx.scene.control.ComboBox categoryCombo = new javafx.scene.control.ComboBox<>(); + categoryCombo.getItems().addAll("Malware", "Pentest", "Network", "Forensics"); + categoryCombo.setPromptText("Catégorie"); + categoryCombo.setMaxWidth(Double.MAX_VALUE); + + javafx.scene.layout.VBox content = new javafx.scene.layout.VBox(10, + new javafx.scene.control.Label("Titre :"), titleField, + new javafx.scene.control.Label("Description :"), descriptionField, + new javafx.scene.control.Label("Catégorie :"), categoryCombo); + content.setPadding(new javafx.geometry.Insets(10)); + dialog.getDialogPane().setContent(content); + + javafx.scene.Node createButton = dialog.getDialogPane().lookupButton(createButtonType); + createButton.setDisable(true); + titleField.textProperty() + .addListener((obs, oldVal, newVal) -> createButton.setDisable(newVal == null || newVal.isBlank())); + + dialog.setResultConverter(button -> { + if (button != createButtonType) { + return null; + } + Lab lab = new Lab(); + lab.setTitle(titleField.getText().trim()); + lab.setDescription(descriptionField.getText()); + lab.setCategory(categoryCombo.getValue() != null ? categoryCombo.getValue() : "Malware"); + return lab; + }); + + dialog.showAndWait().ifPresent(lab -> { + try { + databaseManager.saveLab(lab); + selectedLab = lab; + viewingOrphans = false; + refreshLabList(); + appendJournalLine("Labo \"" + lab.getTitle() + "\" créé"); + + // Proposition d'importer des VM immédiatement (cf. diagramme séquence «Créer + // labo») + javafx.scene.control.Alert prompt = new javafx.scene.control.Alert( + javafx.scene.control.Alert.AlertType.CONFIRMATION); + prompt.setTitle("Labo créé"); + prompt.setHeaderText("Labo \"" + lab.getTitle() + "\" créé avec succès !"); + prompt.setContentText("Voulez-vous importer des VM dans ce labo maintenant ?"); + + javafx.scene.control.ButtonType btnOui = new javafx.scene.control.ButtonType("Oui, importer des VM", + javafx.scene.control.ButtonBar.ButtonData.YES); + javafx.scene.control.ButtonType btnNon = new javafx.scene.control.ButtonType("Non, plus tard", + javafx.scene.control.ButtonBar.ButtonData.NO); + prompt.getButtonTypes().setAll(btnOui, btnNon); + + prompt.showAndWait().ifPresent(response -> { + if (response == btnOui) { + onImportVMsClicked(); + } + }); + + } catch (Exception e) { + showError("Erreur création labo", e.getMessage()); + } + }); + } + + @FXML + private void handleImportVM() { + onImportVMsClicked(); + } + + @FXML + private void handleExportPDF() { + if (selectedLab == null) { + return; + } + + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("Exporter le rapport PDF"); + fileChooser.getExtensionFilters().addAll( + new FileChooser.ExtensionFilter("PDF Document", "*.pdf")); + fileChooser.setInitialFileName(selectedLab.getTitle().replace(" ", "_") + ".pdf"); + + File selectedFile = fileChooser.showSaveDialog( + labListContainer.getScene().getWindow()); + + if (selectedFile == null) { + return; + } + + onExportLabClicked(selectedLab, selectedFile.getAbsolutePath()); + } + + @FXML + private void handleStartVM() { + if (selectedVM == null) { + return; + } + + btnStart.setDisable(true); + + onStartVMClicked(selectedVM, + () -> { + btnStart.setDisable(false); + selectedVM.setStatus(VMStatus.RUNNING); + persisterStatutVM(selectedVM); + updateControlButtons(VMStatus.RUNNING); + refreshVmTable(getVMsForLab(selectedLab.getId())); + appendJournalLine("VM démarrée avec succès"); + persistAuditEntry(selectedVM.getId(), "Start VM", "VM démarrée", selectedLab.getTitle()); + }, + errorMsg -> { + btnStart.setDisable(false); + showError("Erreur démarrage VM", errorMsg); + }); + } + + @FXML + private void handleStopVM() { + if (selectedVM == null) { + return; + } + + btnStop.setDisable(true); + + onStopVMClicked(selectedVM, + () -> { + btnStop.setDisable(false); + selectedVM.setStatus(VMStatus.POWERED_OFF); + persisterStatutVM(selectedVM); + updateControlButtons(VMStatus.POWERED_OFF); + refreshVmTable(getVMsForLab(selectedLab.getId())); + appendJournalLine("VM arrêtée"); + persistAuditEntry(selectedVM.getId(), "Stop VM", "VM arrêtée", selectedLab.getTitle()); + }, + errorMsg -> { + btnStop.setDisable(false); + showError("Erreur arrêt VM", errorMsg); + }); + } + + @FXML + private void handleSaveState() { + if (selectedVM == null) { + return; + } + + btnSaveState.setDisable(true); + + onSaveStateClicked(selectedVM, + () -> { + btnSaveState.setDisable(false); + selectedVM.setStatus(VMStatus.SAVED); + persisterStatutVM(selectedVM); + updateControlButtons(VMStatus.SAVED); + refreshVmTable(getVMsForLab(selectedLab.getId())); + appendJournalLine("État de la VM sauvegardé"); + persistAuditEntry(selectedVM.getId(), "Save State", "État sauvegardé", selectedLab.getTitle()); + }, + errorMsg -> { + btnSaveState.setDisable(false); + showError("Erreur sauvegarde état", errorMsg); + }); } /** - * Retourne tous les laboratoires connus. - * - * @return liste des laboratoires + * Persiste le statut courant d'une VM en base sans modifier son labo de + * rattachement. */ - public List getAllLabs() { - return databaseManager.getAllLabs(); + private void persisterStatutVM(VirtualMachine vm) { + try { + Integer labId = (selectedLab != null) ? selectedLab.getId() : null; + databaseManager.saveVirtualMachine(vm, labId); + } catch (Exception e) { + appendJournalLine("⚠️ Impossible de sauvegarder le statut en base : " + e.getMessage()); + } + } + + @FXML + private void handleTakeSnapshot() { + if (selectedVM == null) { + return; + } + + javafx.scene.control.TextInputDialog dialog = new javafx.scene.control.TextInputDialog(); + dialog.setTitle("Nouveau snapshot"); + dialog.setHeaderText("Nom du snapshot"); + dialog.setContentText("Nom :"); + + dialog.showAndWait().ifPresent(name -> { + if (name.isBlank()) { + return; + } + + onTakeSnapshotClicked(selectedVM, name, "", + () -> { + Snapshot snapshot = new Snapshot(); + snapshot.setName(name); + snapshot.setDescription(""); + snapshot.setCreatedAt(LocalDateTime.now()); + snapshot.setOnline(selectedVM.getStatus() == VMStatus.RUNNING); + + selectedVM.addSnapshot(snapshot); + databaseManager.saveSnapshot(snapshot, selectedVM.getId()); + + snapshotCombo.getItems().add(name); + refreshVmTable(getVMsForLab(selectedLab.getId())); + appendJournalLine("Snapshot \"" + name + "\" created"); + persistAuditEntry(selectedVM.getId(), "Take Snapshot", + "Snapshot \"" + name + "\" créé", selectedLab.getTitle()); + }, + errorMsg -> showError("Erreur snapshot", errorMsg)); + }); + } + + @FXML + private void handleRestoreSnapshot() { + if (selectedVM == null) { + return; + } + + String snapshotName = snapshotCombo.getValue(); + if (snapshotName == null) { + return; + } + + javafx.scene.control.Alert confirm = new javafx.scene.control.Alert( + javafx.scene.control.Alert.AlertType.CONFIRMATION); + confirm.setTitle("Confirmer la restauration"); + confirm.setHeaderText(null); + confirm.setContentText("Restaurer le snapshot \"" + snapshotName + "\" ?\nL'état actuel de la VM sera perdu."); + + confirm.showAndWait().ifPresent(response -> { + if (response != javafx.scene.control.ButtonType.OK) { + return; + } + + final VirtualMachine vmToRestore = selectedVM; + final String labTitle = selectedLab != null ? selectedLab.getTitle() : ""; + + onRestoreSnapshotClicked(vmToRestore, snapshotName, + () -> { + // Après restauration, la VM est éteinte + vmToRestore.setStatus(VMStatus.POWERED_OFF); + persisterStatutVM(vmToRestore); + updateControlButtons(VMStatus.POWERED_OFF); + refreshVmTable(getVMsForLab(selectedLab.getId())); + appendJournalLine("Snapshot \"" + snapshotName + "\" restauré"); + persistAuditEntry(vmToRestore.getId(), "Restore Snapshot", + "Snapshot \"" + snapshotName + "\" restauré", labTitle); + }, + errorMsg -> { + // Fallback: la restauration peut échouer si la session VBox est encore + // verrouillée mais la VM est en fait dans l'état attendu + executor.submit(() -> { + try { + VMStatus live = hypervisor.getStatus(vmToRestore.getUuid()); + if (live == VMStatus.POWERED_OFF || live == VMStatus.SAVED) { + javafx.application.Platform.runLater(() -> { + vmToRestore.setStatus(live); + persisterStatutVM(vmToRestore); + updateControlButtons(live); + refreshVmTable(getVMsForLab(selectedLab.getId())); + appendJournalLine("Snapshot restauré (état: " + live.name() + ")"); + }); + return; + } + } catch (Exception ignored) { + } + javafx.application.Platform.runLater( + () -> showError("Erreur restauration", errorMsg)); + }); + }); + }); + } + + @FXML + private void handleAddJournalEntry() { + String text = journalInputField.getText(); + if (text == null || text.isBlank()) { + return; + } + + appendJournalLine(text); + journalInputField.clear(); + + if (selectedVM != null) { + try { + JournalEntry entry = new JournalEntry(); + entry.setContent(text); + entry.setTimestamp(LocalDateTime.now()); + + databaseManager.addJournalEntry(entry, selectedVM.getId()); + + appendJournalLine("✅ Note sauvegardée en base"); + } catch (Exception e) { + appendJournalLine("❌ Erreur sauvegarde: " + e.getMessage()); + showError("Erreur journal", e.getMessage()); + } + } else { + appendJournalLine("⚠️ Aucune VM sélectionnée - note non sauvegardée"); + } + } + + @FXML + private void handleToggleAuditLogs() { + auditExpanded = !auditExpanded; + auditArea.setVisible(auditExpanded); + auditArea.setManaged(auditExpanded); + auditArrow.setText(auditExpanded ? "⌄" : "›"); + } + + public void shutdown() { + executor.shutdownNow(); + } + + private void refreshVmTable(List vms) { + vmListContainer.getChildren().clear(); + + if (vms.isEmpty()) { + vmEmptyLabel.setVisible(true); + vmEmptyLabel.setManaged(true); + statTotal.setText("0"); + statRunning.setText("0"); + statSnapshots.setText("0"); + return; + } + + vmEmptyLabel.setVisible(false); + vmEmptyLabel.setManaged(false); + + // Charger les snapshots depuis la DB en premier pour que les stats soient + // correctes + for (VirtualMachine vm : vms) { + if (vm.getSnapshots().isEmpty() && vm.getId() != 0) { + databaseManager.getSnapshotsForVM(vm.getId()).forEach(vm::addSnapshot); + } + } + + long runningCount = vms.stream() + .filter(v -> v.getStatus() == VMStatus.RUNNING) + .count(); + long snapCount = vms.stream() + .mapToLong(v -> v.getSnapshots().size()) + .sum(); + + statTotal.setText(String.valueOf(vms.size())); + statRunning.setText(String.valueOf(runningCount)); + statSnapshots.setText(String.valueOf(snapCount)); + + for (VirtualMachine vm : vms) { + String shortUuid = vm.getUuid() != null && vm.getUuid().length() > 8 + ? vm.getUuid().substring(0, 8) + "..." + : vm.getUuid(); + + String statusText = switch (vm.getStatus()) { + case RUNNING -> + "Running"; + case POWERED_OFF -> + "Stopped"; + case SAVED -> + "Saved"; + case PAUSED -> + "Paused"; + default -> + "Unknown"; + }; + + javafx.scene.layout.HBox row = VmTableView.createVmRow( + vm.getName(), shortUuid, statusText, ""); + + row.setOnMouseClicked(e -> onVmRowClicked(vm)); + vmListContainer.getChildren().add(row); + } + } + + private void onLabCardClicked(Lab lab) { + // Double-clic sur le labo déjà sélectionné → désélection (retour à l'état + // neutre) + if (!viewingOrphans && selectedLab != null && selectedLab.getId() == lab.getId()) { + selectedLab = null; + selectedVM = null; + viewingOrphans = false; + labTitleLabel.setText("Sélectionnez un laboratoire"); + btnExportPdf.setVisible(false); + btnExportPdf.setManaged(false); + statsBar.setVisible(false); + statsBar.setManaged(false); + vmTableHeader.setVisible(false); + vmTableHeader.setManaged(false); + vmListContainer.getChildren().clear(); + controlsPlaceholder.setVisible(true); + controlsPlaceholder.setManaged(true); + btnStart.setVisible(false); + btnStart.setManaged(false); + btnStop.setVisible(false); + btnStop.setManaged(false); + btnSaveState.setVisible(false); + btnSaveState.setManaged(false); + btnTakeSnapshot.setVisible(false); + btnTakeSnapshot.setManaged(false); + btnAssignLab.setVisible(false); + btnAssignLab.setManaged(false); + refreshLabList(); + return; + } + + this.selectedLab = lab; + this.selectedVM = null; + this.viewingOrphans = false; + + labTitleLabel.setText(lab.getTitle()); + + btnExportPdf.setVisible(true); + btnExportPdf.setManaged(true); + statsBar.setVisible(true); + statsBar.setManaged(true); + vmTableHeader.setVisible(true); + vmTableHeader.setManaged(true); + + selectedVmNameLabel.setText(""); + controlsPlaceholder.setVisible(true); + controlsPlaceholder.setManaged(true); + btnStart.setVisible(false); + btnStart.setManaged(false); + btnStop.setVisible(false); + btnStop.setManaged(false); + btnSaveState.setVisible(false); + btnSaveState.setManaged(false); + btnTakeSnapshot.setVisible(false); + btnTakeSnapshot.setManaged(false); + btnAssignLab.setVisible(false); + btnAssignLab.setManaged(false); + + refreshLabList(); + refreshVmTable(getVMsForLab(lab.getId())); + } + + private void onOrphanCardClicked(List orphans) { + this.selectedLab = null; + this.selectedVM = null; + this.viewingOrphans = true; + + labTitleLabel.setText("VMs sans labo"); + + btnExportPdf.setVisible(false); + btnExportPdf.setManaged(false); + statsBar.setVisible(true); + statsBar.setManaged(true); + vmTableHeader.setVisible(true); + vmTableHeader.setManaged(true); + + selectedVmNameLabel.setText(""); + controlsPlaceholder.setVisible(true); + controlsPlaceholder.setManaged(true); + btnStart.setVisible(false); + btnStart.setManaged(false); + btnStop.setVisible(false); + btnStop.setManaged(false); + btnSaveState.setVisible(false); + btnSaveState.setManaged(false); + btnTakeSnapshot.setVisible(false); + btnTakeSnapshot.setManaged(false); + btnAssignLab.setVisible(false); + btnAssignLab.setManaged(false); + + refreshLabList(); + refreshVmTable(orphans); + } + + private void onVmRowClicked(VirtualMachine vm) { + this.selectedVM = vm; + + selectedVmNameLabel.setText(vm.getName()); + + controlsPlaceholder.setVisible(false); + controlsPlaceholder.setManaged(false); + + updateControlButtons(vm.getStatus()); + + // Interroger l'hyperviseur en arrière-plan pour synchroniser l'état réel + executor.submit(() -> { + try { + VMStatus liveStatus = hypervisor.getStatus(vm.getUuid()); + if (liveStatus != null && liveStatus != VMStatus.UNKNOWN) { + javafx.application.Platform.runLater(() -> { + if (selectedVM == vm) { + vm.setStatus(liveStatus); + updateControlButtons(liveStatus); + } + }); + } + } catch (Exception ignored) { + } + }); + } + + private void updateControlButtons(VMStatus status) { + boolean isRunning = status == VMStatus.RUNNING; + boolean isStopped = status == VMStatus.POWERED_OFF || status == VMStatus.SAVED; + boolean isUnknown = status == VMStatus.UNKNOWN || status == null; + + btnStart.setVisible(isStopped || isUnknown); + btnStart.setManaged(isStopped || isUnknown); + btnStop.setVisible(isRunning || isUnknown); + btnStop.setManaged(isRunning || isUnknown); + btnSaveState.setVisible(isRunning); + btnSaveState.setManaged(isRunning); + btnTakeSnapshot.setVisible(true); + btnTakeSnapshot.setManaged(true); + + // Le bouton d'assignation change de texte selon le contexte + if (viewingOrphans) { + btnAssignLab.setText("🔗 Assigner au labo"); + } else { + btnAssignLab.setText("🔗 Changer de labo / Détacher"); + } + btnAssignLab.setVisible(true); + btnAssignLab.setManaged(true); } /** - * Retourne les machines virtuelles non rattachees a un laboratoire. - * - * @return liste des machines virtuelles orphelines + * Ouvre un dialog de sélection de laboratoire pour rattacher la VM + * sélectionnée à un labo existant, ou la détacher (orpheline). */ - public List getOrphanVMs() { - return databaseManager.getOrphanVMs(); + @FXML + private void handleAssignLabToVM() { + if (selectedVM == null) { + return; + } + + List labs = databaseManager.getAllLabs(); + if (labs.isEmpty()) { + showError("Aucun labo disponible", + "Créez d'abord un laboratoire avant d'assigner une VM."); + return; + } + + // Options : tous les labs + option « Détacher (orpheline) » + javafx.scene.control.ChoiceDialog dialog = new javafx.scene.control.ChoiceDialog<>(); + dialog.setTitle("Assigner au labo"); + dialog.setHeaderText("VM : " + selectedVM.getName()); + dialog.setContentText("Choisissez le laboratoire de destination :"); + + for (Lab lab : labs) { + dialog.getItems().add(lab.getId() + " — " + lab.getTitle()); + } + dialog.getItems().add("— Détacher (orpheline)"); + dialog.setSelectedItem(dialog.getItems().get(0)); + + dialog.showAndWait().ifPresent(choice -> { + if (choice == null) { + return; + } + + Integer targetLabId = null; + String targetLabName = null; + + if (!choice.startsWith("—")) { + // Extraire l'ID depuis le format "ID — Titre" + try { + int sepIdx = choice.indexOf(" — "); + int labId = Integer.parseInt(choice.substring(0, sepIdx).trim()); + for (Lab lab : labs) { + if (lab.getId() == labId) { + targetLabId = labId; + targetLabName = lab.getTitle(); + break; + } + } + } catch (NumberFormatException ignored) { + return; + } + } + + final Integer finalLabId = targetLabId; + final String finalLabName = targetLabName; + + try { + databaseManager.saveVirtualMachine(selectedVM, finalLabId); + + // Rafraîchir l'affichage selon le contexte actif + if (viewingOrphans) { + refreshVmTable(databaseManager.getOrphanVMs()); + } else if (selectedLab != null) { + refreshVmTable(databaseManager.getVMsForLab(selectedLab.getId())); + } + refreshLabList(); + + String msg = finalLabName != null + ? "VM \"" + selectedVM.getName() + "\" assignée au labo \"" + finalLabName + "\"" + : "VM \"" + selectedVM.getName() + "\" détachée (orpheline)"; + appendJournalLine(msg); + persistAuditEntry(selectedVM.getId(), "Assign Lab", msg, + finalLabName != null ? finalLabName : null); + + } catch (Exception e) { + showError("Erreur assignation", e.getMessage()); + } + }); + } + + private void appendJournalLine(String text) { + String time = java.time.LocalTime.now() + .format(java.time.format.DateTimeFormatter.ofPattern("HH:mm:ss")); + journalArea.appendText("[" + time + "] " + text + "\n"); + } + + private void showError(String title, String msg) { + javafx.scene.control.Alert alert = new javafx.scene.control.Alert( + javafx.scene.control.Alert.AlertType.ERROR); + alert.setTitle(title); + alert.setHeaderText(null); + alert.setContentText(msg); + alert.showAndWait(); } /** - * Persiste la configuration mise à jour dans la base et dans l'appel. + * Trace une action dans la zone d'audit visuelle ET la persiste en base + * via {@link DatabaseManager#addAuditEntry(AuditEntry)}. * - * @param newConfig nouvelle configuration applicative + * @param vmId identifiant local de la VM concernée, ou {@code null} + * @param action action auditée (ex: "Start VM") + * @param details détails complémentaires de l'action + * @param labName nom du laboratoire concerné, ou {@code null} */ - public void onConfigUpdated(AppConfig newConfig) throws HypervisorException { + private void persistAuditEntry(Integer vmId, String action, String details, String labName) { + AuditEntry entry = new AuditEntry(); + entry.setTimestamp(LocalDateTime.now()); + entry.setVmId(vmId); + entry.setAction(action); + entry.setDetails(details); + entry.setLabName(labName); + + auditArea.appendText(entry.getLog() + "\n"); + try { - hypervisor.applyConfig(newConfig); - } catch (HypervisorException e) { - throw new RuntimeException(e); + databaseManager.addAuditEntry(entry); + } catch (Exception e) { + appendJournalLine("⚠️ Erreur persistance audit : " + e.getMessage()); } - databaseManager.saveConfig(newConfig); } -} +} \ No newline at end of file diff --git a/src/main/java/tg/cyberlabmanager/controller/LabController.java.bak b/src/main/java/tg/cyberlabmanager/controller/LabController.java.bak new file mode 100644 index 0000000..d343847 --- /dev/null +++ b/src/main/java/tg/cyberlabmanager/controller/LabController.java.bak @@ -0,0 +1,765 @@ +package tg.cyberlabmanager.controller; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.stage.FileChooser; +import java.io.File; + +import tg.cyberlabmanager.data.DatabaseManager; +import tg.cyberlabmanager.hypervisor.IHypervisor; +import tg.cyberlabmanager.model.AppConfig; +import tg.cyberlabmanager.model.Lab; +import tg.cyberlabmanager.model.VirtualMachine; +import tg.cyberlabmanager.model.Snapshot; +import tg.cyberlabmanager.pdf.PdfExporter; +import tg.cyberlabmanager.model.VMStatus; +import tg.cyberlabmanager.ui.LabListView; +import tg.cyberlabmanager.ui.VmTableView; +import tg.cyberlabmanager.model.JournalEntry; + +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Consumer; +import java.util.ArrayList; +import static tg.cyberlabmanager.model.VMStatus.PAUSED; +import static tg.cyberlabmanager.model.VMStatus.POWERED_OFF; +import static tg.cyberlabmanager.model.VMStatus.RUNNING; +import static tg.cyberlabmanager.model.VMStatus.SAVED; + +/** + * Contrôleur principal responsable de l'affichage de l'interface (Rôle 1). Il + * gère les données de la maquette et agit comme relais visuel. + */ +public class LabController { + + private IHypervisor hypervisor; + private DatabaseManager databaseManager; + private PdfExporter pdfExporter; + private Lab selectedLab; + private VirtualMachine selectedVM; + private boolean auditExpanded = false; + + private final ExecutorService executor = Executors.newCachedThreadPool(); + + @FXML + private Label statusLabel; + @FXML + private javafx.scene.layout.VBox labListContainer; + @FXML + private Label labTitleLabel; + @FXML + private javafx.scene.control.Button btnExportPdf; + @FXML + private javafx.scene.layout.HBox statsBar; + @FXML + private Label statTotal; + @FXML + private Label statRunning; + @FXML + private Label statSnapshots; + @FXML + private javafx.scene.layout.HBox vmTableHeader; + @FXML + private javafx.scene.layout.VBox vmListContainer; + @FXML + private Label vmEmptyLabel; + @FXML + private Label selectedVmNameLabel; + @FXML + private javafx.scene.control.Button btnStart; + @FXML + private javafx.scene.control.Button btnStop; + @FXML + private javafx.scene.control.Button btnSaveState; + @FXML + private javafx.scene.control.Button btnTakeSnapshot; + @FXML + private Label controlsPlaceholder; + @FXML + private javafx.scene.control.ComboBox snapshotCombo; + @FXML + private javafx.scene.control.TextArea journalArea; + @FXML + private javafx.scene.layout.VBox snapshotSection; + @FXML + private javafx.scene.control.TextField journalInputField; + @FXML + private javafx.scene.control.TextArea auditArea; + @FXML + private Label auditArrow; + + @FXML + private void initialize() { + // TODO (Rôle 3) : remplacer par la vraie implémentation dès qu'elle est livrée + this.hypervisor = new tg.cyberlabmanager.hypervisor.MockHypervisor(); + + // Initialiser le DatabaseManager + this.databaseManager = new DatabaseManager(); + + // Initialiser le PdfExporter + this.pdfExporter = new PdfExporter(); + + mockLabs(); + refreshLabList(); + + } + + // ── Données mock temporaires ────────────────────────────────────────── + private List mockLabs() { + List labs = new ArrayList<>(); + labs.add(new Lab(1, "APT29 Analysis", "Analyse APT", "Malware")); + labs.add(new Lab(2, "Red Team Ops", "Pentest réseau", "Pentest")); + labs.add(new Lab(3, "Network Traffic Analysis", "Analyse trafic", "Network")); + labs.add(new Lab(4, "Memory Forensics Lab", "Forensique mémoire", "Forensics")); + labs.add(new Lab(5, "Ransomware Sandbox", "Sandbox malware", "Malware")); + return labs; + } + + + +// ── Affichage de la liste des labos ────────────────────────────────── + private void refreshLabList() { + labListContainer.getChildren().clear(); + + // TODO (Rôle 2) : remplacer mockLabs() par databaseManager.getAllLabs() + List labs = databaseManager.getAllLabs(); + + for (Lab lab : labs) { + boolean isSelected = selectedLab != null && selectedLab.getId() == lab.getId(); + + String badgeStyle = switch (lab.getCategory().toLowerCase()) { + case "malware" -> + "badge-malware"; + case "pentest" -> + "badge-pentest"; + case "forensics" -> + "badge-forensics"; + case "network" -> + "badge-network"; + default -> + "badge-default"; + }; + + javafx.scene.layout.VBox card = LabListView.createLabCard( + lab.getTitle(), lab.getCategory(), badgeStyle, isSelected + ); + + // Listener de clic — Jour 2 + card.setOnMouseClicked(e -> onLabCardClicked(lab)); + + labListContainer.getChildren().add(card); + } + } + + /** + * Constructeur par défaut initialisant le contrôleur du laboratoire. + */ + public LabController() { + } + + public void setDependencies(IHypervisor hypervisor, + DatabaseManager databaseManager, + PdfExporter pdfExporter) { + this.hypervisor = hypervisor; + this.databaseManager = databaseManager; + this.pdfExporter = pdfExporter; + } + + public LabController(IHypervisor hypervisor, DatabaseManager databaseManager, PdfExporter pdfExporter) { + this.hypervisor = hypervisor; + this.databaseManager = databaseManager; + this.pdfExporter = pdfExporter; + } + + public void initView() { + + } + + private List getVMsForLab(int labId) { + // TODO (Rôle 2) : remplacer par databaseManager.getVMsForLab(labId) + List vms = databaseManager.getVMsForLab(labId); + + // Fallback vers les mocks si la base est vide + if (vms.isEmpty()) { + vms = mockVMsForLab(labId); + } + + return vms; +} + + public List getAllLabs() { + throw new UnsupportedOperationException("À implémenter après merge Rôle 2"); + } + + public List getOrphanVMs() { + throw new UnsupportedOperationException("À implémenter après merge Rôle 2"); + } + + public void onStartVMClicked(VirtualMachine vm, + Runnable onSuccess, + Consumer onError) { + + executor.submit(() -> { + try { + hypervisor.startVM(vm.getUuid()); + javafx.application.Platform.runLater(() -> { + vm.setStatus(VMStatus.RUNNING); + onSuccess.run(); + }); + } catch (Exception e) { + javafx.application.Platform.runLater(() -> onError.accept(e.getMessage())); + } + }); + } + + public void onStopVMClicked(VirtualMachine vm, + Runnable onSuccess, + Consumer onError) { + + executor.submit(() -> { + try { + hypervisor.stopVM(vm.getUuid()); + javafx.application.Platform.runLater(() -> { + vm.setStatus(VMStatus.POWERED_OFF); + onSuccess.run(); + }); + } catch (Exception e) { + javafx.application.Platform.runLater(() -> onError.accept(e.getMessage())); + } + }); + } + + public void onSaveStateClicked(VirtualMachine vm, + Runnable onSuccess, + Consumer onError) { + + executor.submit(() -> { + try { + hypervisor.saveState(vm.getUuid()); + javafx.application.Platform.runLater(() -> { + vm.setStatus(VMStatus.SAVED); + onSuccess.run(); + }); + } catch (Exception e) { + javafx.application.Platform.runLater(() -> onError.accept(e.getMessage())); + } + }); + } + + public void onTakeSnapshotClicked(VirtualMachine vm, + String name, + String description, + Runnable onSuccess, + Consumer onError) { + + executor.submit(() -> { + try { + hypervisor.takeSnapshot(vm.getUuid(), name, description); + javafx.application.Platform.runLater(onSuccess); + } catch (Exception e) { + javafx.application.Platform.runLater(() -> onError.accept(e.getMessage())); + } + }); + } + + public void onRestoreSnapshotClicked(VirtualMachine vm, + String snapshotName, + Runnable onSuccess, + Consumer onError) { + + executor.submit(() -> { + try { + hypervisor.restoreSnapshot(vm.getUuid(), snapshotName); + javafx.application.Platform.runLater(onSuccess); + } catch (Exception e) { + javafx.application.Platform.runLater(() -> onError.accept(e.getMessage())); + } + }); + } + + public void onAddJournalEntry(VirtualMachine vm, String text) { + + } + + public void onImportVMsClicked(Consumer> onSuccess, + Consumer onError) { + + executor.submit(() -> { + try { + Thread.sleep(800); // simule le temps d'import + javafx.application.Platform.runLater(() -> { + VirtualMachine imported = new VirtualMachine(); + imported.setName("Imported-VM-" + (System.currentTimeMillis() % 1000)); + imported.setUuid(java.util.UUID.randomUUID().toString()); + imported.setStatus(VMStatus.POWERED_OFF); + + List result = new ArrayList<>(); + result.add(imported); + onSuccess.accept(result); + }); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + javafx.application.Platform.runLater(() -> onError.accept("Import interrompu")); + } + }); + } + + public void onExportLabClicked(Lab lab, String filePath) { + // TODO (Rôle 2) : remplacer par un appel réel à pdfExporter.exportLabReport() + executor.submit(() -> { + try { + // Récupérer les données depuis la base + List vms = databaseManager.getVMsForLab(lab.getId()); + + // Récupérer les snapshots pour chaque VM + List allSnapshots = new ArrayList<>(); + for (VirtualMachine vm : vms) { + allSnapshots.addAll(databaseManager.getSnapshotsForVM(vm.getId())); + } + + // Récupérer les journaux pour chaque VM + List allJournals = new ArrayList<>(); + for (VirtualMachine vm : vms) { + allJournals.addAll(databaseManager.getJournalEntriesForVM(vm.getId())); + } + + // Exporter le PDF + pdfExporter.exportLabToPdf(lab, vms, allSnapshots, allJournals, filePath); + + // Succès - mise à jour de l'UI + javafx.application.Platform.runLater(() -> { + appendJournalLine("✅ Export PDF terminé : " + filePath); + appendAuditLine("Export PDF du labo \"" + lab.getTitle() + "\""); + }); + + } catch (Exception e) { + // Erreur - affichage dans l'UI + javafx.application.Platform.runLater(() -> { + appendJournalLine("❌ Erreur export PDF : " + e.getMessage()); + showError("Erreur export PDF", e.getMessage()); + }); + } + }); +} + + public void onExportOrphansClicked(String filePath) { + } + + /** + * Persiste la configuration mise a jour. + * + * @param newConfig nouvelle configuration applicative + */ + public void onConfigUpdated(AppConfig newConfig) { + } + + @FXML + private void handleImportVM() { + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("Importer une VM"); + fileChooser.getExtensionFilters().addAll( + new FileChooser.ExtensionFilter("VirtualBox Appliance", "*.ova"), + new FileChooser.ExtensionFilter("Disque virtuel", "*.vdi") + ); + + File selectedFile = fileChooser.showOpenDialog( + labListContainer.getScene().getWindow() + ); + + if (selectedFile == null) { + return; + } + + // TODO (Rôle 3) : remplacer par un appel réel à hypervisor.importVM() + onImportVMsClicked( + vms -> { + if (selectedLab != null) { + getVMsForLab(selectedLab.getId()).addAll(vms); + refreshVmTable(getVMsForLab(selectedLab.getId())); + } + appendJournalLine("Import terminé : " + selectedFile.getName()); + appendAuditLine("VM importée depuis " + selectedFile.getName()); + }, + errorMsg -> showError("Erreur d'import", errorMsg) + ); + } + + @FXML + private void handleExportPDF() { + // Le bouton Export PDF n'est visible que si un labo est sélectionné, + // mais on vérifie par précaution. + if (selectedLab == null) { + return; + } + + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("Exporter le rapport PDF"); + fileChooser.getExtensionFilters().addAll( + new FileChooser.ExtensionFilter("PDF Document", "*.pdf") + ); + // Suggestion de nom de fichier basé sur le titre du labo + fileChooser.setInitialFileName(selectedLab.getTitle().replace(" ", "_") + ".pdf"); + + File selectedFile = fileChooser.showSaveDialog( + labListContainer.getScene().getWindow() + ); + + if (selectedFile == null) { + return; // L'utilisateur a annulé + } + + // TODO (Rôle 2) : remplacer par un appel réel à pdfExporter.exportLabReport() + // Pour l'instant, on appelle le stub métier (déjà présent dans LabController) + onExportLabClicked(selectedLab, selectedFile.getAbsolutePath()); + + } + + @FXML + private void handleStartVM() { + if (selectedVM == null) { + return; + } + + btnStart.setDisable(true); + + onStartVMClicked(selectedVM, + () -> { + btnStart.setDisable(false); + updateControlButtons(VMStatus.RUNNING); + refreshVmTable(getVMsForLab(selectedLab.getId())); + appendJournalLine("VM started successfully"); + appendAuditLine("VM démarrée"); + }, + errorMsg -> { + btnStart.setDisable(false); + showError("Erreur démarrage VM", errorMsg); + } + ); + } + + @FXML + private void handleStopVM() { + if (selectedVM == null) { + return; + } + + btnStop.setDisable(true); + + onStopVMClicked(selectedVM, + () -> { + btnStop.setDisable(false); + updateControlButtons(VMStatus.POWERED_OFF); + refreshVmTable(getVMsForLab(selectedLab.getId())); + appendJournalLine("VM stopped"); + appendAuditLine("VM arrêtée"); + }, + errorMsg -> { + btnStop.setDisable(false); + showError("Erreur arrêt VM", errorMsg); + } + ); + } + + @FXML + private void handleSaveState() { + if (selectedVM == null) { + return; + } + + btnSaveState.setDisable(true); + + onSaveStateClicked(selectedVM, + () -> { + btnSaveState.setDisable(false); + updateControlButtons(VMStatus.SAVED); + refreshVmTable(getVMsForLab(selectedLab.getId())); + appendJournalLine("VM state saved"); + appendAuditLine("État sauvegardé"); + }, + errorMsg -> { + btnSaveState.setDisable(false); + showError("Erreur sauvegarde état", errorMsg); + } + ); + } + + @FXML + private void handleTakeSnapshot() { + if (selectedVM == null) { + return; + } + + javafx.scene.control.TextInputDialog dialog = new javafx.scene.control.TextInputDialog(); + dialog.setTitle("Nouveau snapshot"); + dialog.setHeaderText("Nom du snapshot"); + dialog.setContentText("Nom :"); + + dialog.showAndWait().ifPresent(name -> { + if (name.isBlank()) { + return; + } + + onTakeSnapshotClicked(selectedVM, name, "", + () -> { + tg.cyberlabmanager.model.Snapshot snapshot = new tg.cyberlabmanager.model.Snapshot(); + snapshot.setName(name); + snapshot.setDescription(""); + snapshot.setCreatedAt(java.time.LocalDateTime.now()); + snapshot.setOnline(selectedVM.getStatus() == VMStatus.RUNNING); + + selectedVM.addSnapshot(snapshot); + snapshotCombo.getItems().add(name); + refreshVmTable(getVMsForLab(selectedLab.getId())); + appendJournalLine("Snapshot \"" + name + "\" created"); + appendAuditLine("Snapshot \"" + name + "\" créé"); + }, + errorMsg -> showError("Erreur snapshot", errorMsg) + ); + }); + } + + @FXML + private void handleRestoreSnapshot() { + if (selectedVM == null) { + return; + } + + String snapshotName = snapshotCombo.getValue(); + if (snapshotName == null) { + return; + } + + javafx.scene.control.Alert confirm = new javafx.scene.control.Alert( + javafx.scene.control.Alert.AlertType.CONFIRMATION); + confirm.setTitle("Confirmer la restauration"); + confirm.setHeaderText(null); + confirm.setContentText("Restaurer le snapshot \"" + snapshotName + "\" ?\nL'état actuel de la VM sera perdu."); + + confirm.showAndWait().ifPresent(response -> { + if (response != javafx.scene.control.ButtonType.OK) { + return; + } + + onRestoreSnapshotClicked(selectedVM, snapshotName, + () -> { + appendJournalLine("Snapshot \"" + snapshotName + "\" restored"); + appendAuditLine("Snapshot \"" + snapshotName + "\" restauré"); + }, + errorMsg -> showError("Erreur restauration snapshot", errorMsg) + ); + }); + } + + @FXML +private void handleAddJournalEntry() { + String text = journalInputField.getText(); + if (text == null || text.isBlank()) { + return; + } + + appendJournalLine(text); + journalInputField.clear(); + + // TODO (Rôle 2) : databaseManager.addJournalEntry(selectedLab, selectedVM, text) + if (selectedVM != null) { + try { + // Créer une entrée de journal + tg.cyberlabmanager.model.JournalEntry entry = new tg.cyberlabmanager.model.JournalEntry(); + entry.setContent(text); + entry.setTimestamp(java.time.LocalDateTime.now()); + + // Sauvegarder en base + databaseManager.addJournalEntry(entry, selectedVM.getId()); + + appendJournalLine("✅ Note sauvegardée en base"); + } catch (Exception e) { + appendJournalLine("❌ Erreur sauvegarde: " + e.getMessage()); + showError("Erreur journal", e.getMessage()); + } + } else { + appendJournalLine("⚠️ Aucune VM sélectionnée - note non sauvegardée"); + } +} + @FXML + private void handleToggleAuditLogs() { + auditExpanded = !auditExpanded; + auditArea.setVisible(auditExpanded); + auditArea.setManaged(auditExpanded); + auditArrow.setText(auditExpanded ? "⌄" : "›"); + } + + public void shutdown() { + executor.shutdownNow(); + } + + private List mockVMsForLab(int labId) { + List vms = new ArrayList<>(); + + if (labId == 1) { // APT29 Analysis + VirtualMachine vm = new VirtualMachine(); + vm.setName("Kali-APT29"); + vm.setUuid("a1b2c3d4-1111-2222-3333-444455556666"); + vm.setStatus(VMStatus.RUNNING); + vms.add(vm); + } else if (labId == 2) { // Red Team Ops + VirtualMachine vm = new VirtualMachine(); + vm.setName("Metasploitable3"); + vm.setUuid("b2c3d4e5-2222-3333-4444-555566667777"); + vm.setStatus(VMStatus.POWERED_OFF); + vms.add(vm); + } else if (labId == 3) { // Network Traffic Analysis + VirtualMachine vm = new VirtualMachine(); + vm.setName("Wireshark-Station"); + vm.setUuid("f8c9a7b6-3333-4444-5555-666677778888"); + vm.setStatus(VMStatus.RUNNING); + vms.add(vm); + } else if (labId == 4) { // Memory Forensics + VirtualMachine vm = new VirtualMachine(); + vm.setName("REMnux"); + vm.setUuid("c3d4e5f6-4444-5555-6666-777788889999"); + vm.setStatus(VMStatus.SAVED); + vms.add(vm); + } + // labId == 5 (Ransomware Sandbox) → liste vide intentionnellement + return vms; + } + + private void refreshVmTable(List vms) { + vmListContainer.getChildren().clear(); + + if (vms.isEmpty()) { + vmEmptyLabel.setVisible(true); + vmEmptyLabel.setManaged(true); + statTotal.setText("0"); + statRunning.setText("0"); + statSnapshots.setText("0"); + return; + } + + vmEmptyLabel.setVisible(false); + vmEmptyLabel.setManaged(false); + + long runningCount = vms.stream() + .filter(v -> v.getStatus() == VMStatus.RUNNING) + .count(); + long snapCount = vms.stream() + .mapToLong(v -> v.getSnapshots().size()) + .sum(); + + statTotal.setText(String.valueOf(vms.size())); + statRunning.setText(String.valueOf(runningCount)); + statSnapshots.setText(String.valueOf(snapCount)); + + for (VirtualMachine vm : vms) { + String shortUuid = vm.getUuid() != null && vm.getUuid().length() > 8 + ? vm.getUuid().substring(0, 8) + "..." + : vm.getUuid(); + + String statusText = switch (vm.getStatus()) { + case RUNNING -> + "Running"; + case POWERED_OFF -> + "Stopped"; + case SAVED -> + "Saved"; + case PAUSED -> + "Paused"; + default -> + "Unknown"; + }; + + javafx.scene.layout.HBox row = VmTableView.createVmRow( + vm.getName(), shortUuid, statusText, "" + ); + + // Listener clic VM → Jour 3 + row.setOnMouseClicked(e -> onVmRowClicked(vm)); + vmListContainer.getChildren().add(row); + } + } + + private void onLabCardClicked(Lab lab) { + this.selectedLab = lab; + this.selectedVM = null; + + // Mettre à jour le titre + labTitleLabel.setText(lab.getTitle()); + + // Afficher les éléments cachés + btnExportPdf.setVisible(true); + btnExportPdf.setManaged(true); + statsBar.setVisible(true); + statsBar.setManaged(true); + vmTableHeader.setVisible(true); + vmTableHeader.setManaged(true); + + // Remettre le panneau droit en état vide + selectedVmNameLabel.setText(""); + controlsPlaceholder.setVisible(true); + controlsPlaceholder.setManaged(true); + btnStart.setVisible(false); + btnStart.setManaged(false); + btnStop.setVisible(false); + btnStop.setManaged(false); + btnSaveState.setVisible(false); + btnSaveState.setManaged(false); + btnTakeSnapshot.setVisible(false); + btnTakeSnapshot.setManaged(false); + + // Rafraîchir la liste et le tableau + refreshLabList(); + + // TODO (Rôle 2) : remplacer mockVMsForLab par databaseManager.getVMsForLab + refreshVmTable(getVMsForLab(lab.getId())); + } + + private void onVmRowClicked(VirtualMachine vm) { + this.selectedVM = vm; + + selectedVmNameLabel.setText(vm.getName()); + + // Cacher le placeholder "sélectionnez une VM" + controlsPlaceholder.setVisible(false); + controlsPlaceholder.setManaged(false); + + // Activer les bons boutons selon le statut de la VM + updateControlButtons(vm.getStatus()); + } + + private void updateControlButtons(VMStatus status) { + boolean isRunning = status == VMStatus.RUNNING; + boolean isStopped = status == VMStatus.POWERED_OFF || status == VMStatus.SAVED; + + btnStart.setVisible(isStopped); + btnStart.setManaged(isStopped); + btnStop.setVisible(isRunning); + btnStop.setManaged(isRunning); + btnSaveState.setVisible(isRunning); + btnSaveState.setManaged(isRunning); + btnTakeSnapshot.setVisible(true); + btnTakeSnapshot.setManaged(true); + } + + private void appendJournalLine(String text) { + String time = java.time.LocalTime.now() + .format(java.time.format.DateTimeFormatter.ofPattern("HH:mm:ss")); + journalArea.appendText("[" + time + "] " + text + "\n"); + } + + private void showError(String title, String msg) { + javafx.scene.control.Alert alert = new javafx.scene.control.Alert( + javafx.scene.control.Alert.AlertType.ERROR); + alert.setTitle(title); + alert.setHeaderText(null); + alert.setContentText(msg); + alert.showAndWait(); + } + + private void appendAuditLine(String action) { + String time = java.time.LocalTime.now() + .format(java.time.format.DateTimeFormatter.ofPattern("HH:mm:ss")); + String vmName = selectedVM != null ? selectedVM.getName() : "?"; + auditArea.appendText("[" + time + "] [" + vmName + "] " + action + "\n"); + } +} diff --git a/src/main/java/tg/cyberlabmanager/data/DatabaseManager.java b/src/main/java/tg/cyberlabmanager/data/DatabaseManager.java index 833413f..ae19aed 100644 --- a/src/main/java/tg/cyberlabmanager/data/DatabaseManager.java +++ b/src/main/java/tg/cyberlabmanager/data/DatabaseManager.java @@ -5,172 +5,545 @@ import tg.cyberlabmanager.model.JournalEntry; import tg.cyberlabmanager.model.Lab; import tg.cyberlabmanager.model.Snapshot; +import tg.cyberlabmanager.model.VMStatus; import tg.cyberlabmanager.model.VirtualMachine; +import java.sql.*; +import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.List; /** - * Service d'acces aux donnees de l'application. + * Service d'accès aux données de l'application. * - *

Il masque la persistance SQLite derriere des operations de haut niveau sur - * les entites metier.

+ *

Maintient une connexion SQLite unique ouverte pendant toute la durée de + * vie de l'instance. Compatible avec SQLite en mémoire (:memory:) pour les + * tests.

*/ public class DatabaseManager { + + private final Connection connection; + + // ========================================================================= + // Constructeurs + // ========================================================================= + /** - * Cree un gestionnaire de base de donnees. + * Crée un gestionnaire utilisant le fichier cyberlab.db dans le répertoire + * utilisateur. */ public DatabaseManager() { + this("jdbc:sqlite:" + System.getProperty("user.home") + "/cyberlab.db"); } /** - * Enregistre ou met a jour un laboratoire. + * Crée un gestionnaire avec une URL JDBC explicite. + * Utiliser "jdbc:sqlite::memory:" pour les tests unitaires. + * + * @param jdbcUrl URL JDBC SQLite + */ + public DatabaseManager(String jdbcUrl) { + try { + Class.forName("org.sqlite.JDBC"); + this.connection = DriverManager.getConnection(jdbcUrl); + } catch (ClassNotFoundException e) { + throw new RuntimeException("Driver SQLite introuvable", e); + } catch (SQLException e) { + throw new RuntimeException("Impossible d'ouvrir la base de données : " + e.getMessage(), e); + } + initDatabase(); + } + + private Connection getConnection() { + return this.connection; + } + + /** + * Crée toutes les tables si elles n'existent pas encore. + */ + private void initDatabase() { + // Activer les clés étrangères SQLite (désactivées par défaut) + try (Statement stmt = getConnection().createStatement()) { + stmt.execute("PRAGMA foreign_keys = ON"); + } catch (SQLException e) { + throw new RuntimeException("Impossible d'activer les clés étrangères : " + e.getMessage(), e); + } + String[] tables = { + "CREATE TABLE IF NOT EXISTS labo (" + + " id_labo INTEGER PRIMARY KEY AUTOINCREMENT," + + " titre TEXT NOT NULL," + + " description TEXT," + + " categorie TEXT" + + ");", + + "CREATE TABLE IF NOT EXISTS vm (" + + " id_vm INTEGER PRIMARY KEY AUTOINCREMENT," + + " nom_vm TEXT NOT NULL," + + " uuid TEXT UNIQUE," + + " id_labo INTEGER REFERENCES labo(id_labo) ON DELETE SET NULL," + + " lien_doc TEXT," + + " statut TEXT" + + ");", + + "CREATE TABLE IF NOT EXISTS snapshot (" + + " id_snap INTEGER PRIMARY KEY AUTOINCREMENT," + + " id_vm INTEGER NOT NULL REFERENCES vm(id_vm) ON DELETE CASCADE," + + " uuid TEXT," + + " nom_snap TEXT," + + " date_creation TEXT," + + " description TEXT," + + " online INTEGER DEFAULT 0" + + ");", + + "CREATE TABLE IF NOT EXISTS journal_entry (" + + " id_entry INTEGER PRIMARY KEY AUTOINCREMENT," + + " id_vm INTEGER NOT NULL REFERENCES vm(id_vm) ON DELETE CASCADE," + + " horodatage TEXT NOT NULL," + + " contenu TEXT NOT NULL" + + ");", + + "CREATE TABLE IF NOT EXISTS audit_log (" + + " id_log INTEGER PRIMARY KEY AUTOINCREMENT," + + " horodatage TEXT NOT NULL," + + " id_vm INTEGER REFERENCES vm(id_vm) ON DELETE SET NULL," + + " action TEXT NOT NULL," + + " details TEXT," + + " lab_name TEXT" + + ");", + + "CREATE TABLE IF NOT EXISTS app_config (" + + " cle TEXT PRIMARY KEY," + + " valeur TEXT" + + ");" + }; + + try (Statement stmt = getConnection().createStatement()) { + for (String sql : tables) { + stmt.execute(sql); + } + } catch (SQLException e) { + throw new RuntimeException("Impossible d'initialiser la base de données : " + e.getMessage(), e); + } + } + + // ========================================================================= + // LABORATOIRES + // ========================================================================= + + /** + * Enregistre ou met à jour un laboratoire. * - * @param lab laboratoire a persister + * @param lab laboratoire à persister */ public void saveLab(Lab lab) { - throw new UnsupportedOperationException("Not implemented yet"); + if (lab.getId() == 0) { + String sql = "INSERT INTO labo (titre, description, categorie) VALUES (?, ?, ?)"; + try (PreparedStatement ps = getConnection().prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + ps.setString(1, lab.getTitle()); + ps.setString(2, lab.getDescription()); + ps.setString(3, lab.getCategory()); + ps.executeUpdate(); + ResultSet keys = ps.getGeneratedKeys(); + if (keys.next()) lab.setId(keys.getInt(1)); + } catch (SQLException e) { + throw new RuntimeException("Erreur saveLab (INSERT) : " + e.getMessage(), e); + } + } else { + String sql = "UPDATE labo SET titre=?, description=?, categorie=? WHERE id_labo=?"; + try (PreparedStatement ps = getConnection().prepareStatement(sql)) { + ps.setString(1, lab.getTitle()); + ps.setString(2, lab.getDescription()); + ps.setString(3, lab.getCategory()); + ps.setInt(4, lab.getId()); + ps.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException("Erreur saveLab (UPDATE) : " + e.getMessage(), e); + } + } } /** - * Recupere un laboratoire par son identifiant local. + * Récupère un laboratoire par son identifiant local. * * @param id identifiant local du laboratoire - * @return laboratoire correspondant + * @return laboratoire correspondant, ou null si introuvable */ public Lab getLab(int id) { - throw new UnsupportedOperationException("Not implemented yet"); + String sql = "SELECT id_labo, titre, description, categorie FROM labo WHERE id_labo=?"; + try (PreparedStatement ps = getConnection().prepareStatement(sql)) { + ps.setInt(1, id); + ResultSet rs = ps.executeQuery(); + if (rs.next()) return mapLab(rs); + } catch (SQLException e) { + throw new RuntimeException("Erreur getLab : " + e.getMessage(), e); + } + return null; } /** - * Recupere tous les laboratoires. + * Récupère tous les laboratoires. * * @return liste des laboratoires */ public List getAllLabs() { - throw new UnsupportedOperationException("Not implemented yet"); + List labs = new ArrayList<>(); + String sql = "SELECT id_labo, titre, description, categorie FROM labo ORDER BY titre"; + try (Statement stmt = getConnection().createStatement(); + ResultSet rs = stmt.executeQuery(sql)) { + while (rs.next()) labs.add(mapLab(rs)); + } catch (SQLException e) { + throw new RuntimeException("Erreur getAllLabs : " + e.getMessage(), e); + } + return labs; } /** * Supprime un laboratoire sans supprimer les VM physiques. * - * @param lab laboratoire a supprimer + * @param lab laboratoire à supprimer */ public void deleteLab(Lab lab) { - throw new UnsupportedOperationException("Not implemented yet"); + String sql = "DELETE FROM labo WHERE id_labo=?"; + try (PreparedStatement ps = getConnection().prepareStatement(sql)) { + ps.setInt(1, lab.getId()); + ps.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException("Erreur deleteLab : " + e.getMessage(), e); + } } + private Lab mapLab(ResultSet rs) throws SQLException { + Lab lab = new Lab(); + lab.setId(rs.getInt("id_labo")); + lab.setTitle(rs.getString("titre")); + lab.setDescription(rs.getString("description")); + lab.setCategory(rs.getString("categorie")); + return lab; + } + + // ========================================================================= + // MACHINES VIRTUELLES + // ========================================================================= + /** - * Enregistre ou met a jour une machine virtuelle. + * Enregistre ou met à jour une machine virtuelle. * - * @param vm machine virtuelle a persister - * @param labId identifiant du laboratoire rattache, ou {@code null} + * @param vm machine virtuelle à persister + * @param labId identifiant du laboratoire rattaché, ou {@code null} */ public void saveVirtualMachine(VirtualMachine vm, Integer labId) { - throw new UnsupportedOperationException("Not implemented yet"); + String statut = vm.getStatus() != null ? vm.getStatus().name() : VMStatus.UNKNOWN.name(); + + if (vm.getId() == 0) { + String sql = "INSERT INTO vm (nom_vm, uuid, id_labo, lien_doc, statut) VALUES (?,?,?,?,?)"; + try (PreparedStatement ps = getConnection().prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + ps.setString(1, vm.getName()); + ps.setString(2, vm.getUuid()); + if (labId != null) ps.setInt(3, labId); else ps.setNull(3, Types.INTEGER); + ps.setString(4, vm.getDocumentationUrl()); + ps.setString(5, statut); + ps.executeUpdate(); + ResultSet keys = ps.getGeneratedKeys(); + if (keys.next()) vm.setId(keys.getInt(1)); + } catch (SQLException e) { + throw new RuntimeException("Erreur saveVirtualMachine (INSERT) : " + e.getMessage(), e); + } + } else { + String sql = "UPDATE vm SET nom_vm=?, uuid=?, id_labo=?, lien_doc=?, statut=? WHERE id_vm=?"; + try (PreparedStatement ps = getConnection().prepareStatement(sql)) { + ps.setString(1, vm.getName()); + ps.setString(2, vm.getUuid()); + if (labId != null) ps.setInt(3, labId); else ps.setNull(3, Types.INTEGER); + ps.setString(4, vm.getDocumentationUrl()); + ps.setString(5, statut); + ps.setInt(6, vm.getId()); + ps.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException("Erreur saveVirtualMachine (UPDATE) : " + e.getMessage(), e); + } + } } /** - * Recupere les machines virtuelles rattachees a un laboratoire. + * Récupère les machines virtuelles rattachées à un laboratoire. * * @param labId identifiant local du laboratoire - * @return liste des machines virtuelles rattachees + * @return liste des machines virtuelles rattachées */ public List getVMsForLab(int labId) { - throw new UnsupportedOperationException("Not implemented yet"); + List vms = new ArrayList<>(); + String sql = "SELECT * FROM vm WHERE id_labo=?"; + try (PreparedStatement ps = getConnection().prepareStatement(sql)) { + ps.setInt(1, labId); + ResultSet rs = ps.executeQuery(); + while (rs.next()) vms.add(mapVM(rs)); + } catch (SQLException e) { + throw new RuntimeException("Erreur getVMsForLab : " + e.getMessage(), e); + } + return vms; } /** - * Recupere les machines virtuelles sans laboratoire. + * Récupère les machines virtuelles sans laboratoire. * * @return liste des machines virtuelles orphelines */ public List getOrphanVMs() { - throw new UnsupportedOperationException("Not implemented yet"); + List vms = new ArrayList<>(); + String sql = "SELECT * FROM vm WHERE id_labo IS NULL"; + try (Statement stmt = getConnection().createStatement(); + ResultSet rs = stmt.executeQuery(sql)) { + while (rs.next()) vms.add(mapVM(rs)); + } catch (SQLException e) { + throw new RuntimeException("Erreur getOrphanVMs : " + e.getMessage(), e); + } + return vms; } /** - * Detache une machine virtuelle de son laboratoire. + * Détache une machine virtuelle de son laboratoire. * - * @param vm machine virtuelle a detacher + * @param vm machine virtuelle à détacher */ public void removeVMFromLab(VirtualMachine vm) { - throw new UnsupportedOperationException("Not implemented yet"); + String sql = "UPDATE vm SET id_labo=NULL WHERE id_vm=?"; + try (PreparedStatement ps = getConnection().prepareStatement(sql)) { + ps.setInt(1, vm.getId()); + ps.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException("Erreur removeVMFromLab : " + e.getMessage(), e); + } + } + + private VirtualMachine mapVM(ResultSet rs) throws SQLException { + VirtualMachine vm = new VirtualMachine(); + vm.setId(rs.getInt("id_vm")); + vm.setName(rs.getString("nom_vm")); + vm.setUuid(rs.getString("uuid")); + vm.setDocumentationUrl(rs.getString("lien_doc")); + String statut = rs.getString("statut"); + try { + vm.setStatus(statut != null ? VMStatus.valueOf(statut) : VMStatus.UNKNOWN); + } catch (IllegalArgumentException e) { + vm.setStatus(VMStatus.UNKNOWN); + } + return vm; } + // ========================================================================= + // SNAPSHOTS + // ========================================================================= + /** * Enregistre un snapshot pour une machine virtuelle. * - * @param snap snapshot a persister + * @param snap snapshot à persister * @param vmId identifiant local de la machine virtuelle */ public void saveSnapshot(Snapshot snap, int vmId) { - throw new UnsupportedOperationException("Not implemented yet"); + String sql = "INSERT INTO snapshot (id_vm, uuid, nom_snap, date_creation, description, online) VALUES (?,?,?,?,?,?)"; + try (PreparedStatement ps = getConnection().prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + ps.setInt(1, vmId); + ps.setString(2, snap.getUuid()); + ps.setString(3, snap.getName()); + ps.setString(4, snap.getCreatedAt() != null ? snap.getCreatedAt().toString() : null); + ps.setString(5, snap.getDescription()); + ps.setInt(6, snap.isOnline() ? 1 : 0); + ps.executeUpdate(); + ResultSet keys = ps.getGeneratedKeys(); + if (keys.next()) snap.setId(keys.getInt(1)); + } catch (SQLException e) { + throw new RuntimeException("Erreur saveSnapshot : " + e.getMessage(), e); + } } /** - * Recupere les snapshots d'une machine virtuelle. + * Récupère les snapshots d'une machine virtuelle. * * @param vmId identifiant local de la machine virtuelle - * @return liste des snapshots rattaches + * @return liste des snapshots rattachés */ public List getSnapshotsForVM(int vmId) { - throw new UnsupportedOperationException("Not implemented yet"); + List snaps = new ArrayList<>(); + String sql = "SELECT * FROM snapshot WHERE id_vm=? ORDER BY date_creation DESC"; + try (PreparedStatement ps = getConnection().prepareStatement(sql)) { + ps.setInt(1, vmId); + ResultSet rs = ps.executeQuery(); + while (rs.next()) snaps.add(mapSnapshot(rs)); + } catch (SQLException e) { + throw new RuntimeException("Erreur getSnapshotsForVM : " + e.getMessage(), e); + } + return snaps; + } + + private Snapshot mapSnapshot(ResultSet rs) throws SQLException { + Snapshot s = new Snapshot(); + s.setId(rs.getInt("id_snap")); + s.setUuid(rs.getString("uuid")); + s.setName(rs.getString("nom_snap")); + String dateStr = rs.getString("date_creation"); + s.setCreatedAt(dateStr != null ? LocalDateTime.parse(dateStr) : null); + s.setDescription(rs.getString("description")); + s.setOnline(rs.getInt("online") == 1); + return s; } + // ========================================================================= + // JOURNAL DE BORD + // ========================================================================= + /** * Ajoute une note d'analyse pour une machine virtuelle. * - * @param entry note d'analyse a persister - * @param vmId identifiant local de la machine virtuelle + * @param entry note d'analyse à persister + * @param vmId identifiant local de la machine virtuelle */ public void addJournalEntry(JournalEntry entry, int vmId) { - throw new UnsupportedOperationException("Not implemented yet"); + String sql = "INSERT INTO journal_entry (id_vm, horodatage, contenu) VALUES (?,?,?)"; + try (PreparedStatement ps = getConnection().prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + ps.setInt(1, vmId); + ps.setString(2, entry.getTimestamp() != null ? entry.getTimestamp().toString() : LocalDateTime.now().toString()); + ps.setString(3, entry.getContent()); + ps.executeUpdate(); + ResultSet keys = ps.getGeneratedKeys(); + if (keys.next()) entry.setId(keys.getInt(1)); + } catch (SQLException e) { + throw new RuntimeException("Erreur addJournalEntry : " + e.getMessage(), e); + } } /** - * Recupere les notes d'analyse d'une machine virtuelle. + * Récupère les notes d'analyse d'une machine virtuelle. * * @param vmId identifiant local de la machine virtuelle * @return liste des notes d'analyse */ public List getJournalEntriesForVM(int vmId) { - throw new UnsupportedOperationException("Not implemented yet"); + List entries = new ArrayList<>(); + String sql = "SELECT * FROM journal_entry WHERE id_vm=? ORDER BY horodatage DESC"; + try (PreparedStatement ps = getConnection().prepareStatement(sql)) { + ps.setInt(1, vmId); + ResultSet rs = ps.executeQuery(); + while (rs.next()) entries.add(mapJournalEntry(rs)); + } catch (SQLException e) { + throw new RuntimeException("Erreur getJournalEntriesForVM : " + e.getMessage(), e); + } + return entries; } + private JournalEntry mapJournalEntry(ResultSet rs) throws SQLException { + JournalEntry e = new JournalEntry(); + e.setId(rs.getInt("id_entry")); + String dateStr = rs.getString("horodatage"); + e.setTimestamp(dateStr != null ? LocalDateTime.parse(dateStr) : null); + e.setContent(rs.getString("contenu")); + return e; + } + + // ========================================================================= + // AUDIT + // ========================================================================= + /** - * Ajoute une entree d'audit. + * Ajoute une entrée d'audit. * - * @param entry entree d'audit a persister + * @param entry entrée d'audit à persister */ public void addAuditEntry(AuditEntry entry) { - throw new UnsupportedOperationException("Not implemented yet"); + String sql = "INSERT INTO audit_log (horodatage, id_vm, action, details, lab_name) VALUES (?,?,?,?,?)"; + try (PreparedStatement ps = getConnection().prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + ps.setString(1, entry.getTimestamp() != null ? entry.getTimestamp().toString() : LocalDateTime.now().toString()); + if (entry.getVmId() != null) ps.setInt(2, entry.getVmId()); else ps.setNull(2, Types.INTEGER); + ps.setString(3, entry.getAction()); + ps.setString(4, entry.getDetails()); + ps.setString(5, entry.getLabName()); + ps.executeUpdate(); + ResultSet keys = ps.getGeneratedKeys(); + if (keys.next()) entry.setId(keys.getInt(1)); + } catch (SQLException e) { + throw new RuntimeException("Erreur addAuditEntry : " + e.getMessage(), e); + } } /** - * Recupere les entrees d'audit d'une machine virtuelle. + * Récupère les entrées d'audit d'une machine virtuelle. * * @param vmId identifiant local de la machine virtuelle - * @return liste des entrees d'audit + * @return liste des entrées d'audit */ public List getAuditLogsForVM(int vmId) { - throw new UnsupportedOperationException("Not implemented yet"); + List logs = new ArrayList<>(); + String sql = "SELECT * FROM audit_log WHERE id_vm=? ORDER BY horodatage DESC"; + try (PreparedStatement ps = getConnection().prepareStatement(sql)) { + ps.setInt(1, vmId); + ResultSet rs = ps.executeQuery(); + while (rs.next()) logs.add(mapAuditEntry(rs)); + } catch (SQLException e) { + throw new RuntimeException("Erreur getAuditLogsForVM : " + e.getMessage(), e); + } + return logs; + } + + private AuditEntry mapAuditEntry(ResultSet rs) throws SQLException { + AuditEntry a = new AuditEntry(); + a.setId(rs.getInt("id_log")); + String dateStr = rs.getString("horodatage"); + a.setTimestamp(dateStr != null ? LocalDateTime.parse(dateStr) : null); + int vmIdValue = rs.getInt("id_vm"); + a.setVmId(rs.wasNull() ? null : vmIdValue); + a.setAction(rs.getString("action")); + a.setDetails(rs.getString("details")); + a.setLabName(rs.getString("lab_name")); + return a; } + // ========================================================================= + // CONFIGURATION + // ========================================================================= + /** - * Recupere la configuration applicative. + * Récupère la configuration applicative. * * @return configuration applicative */ public AppConfig getConfig() { - throw new UnsupportedOperationException("Not implemented yet"); + AppConfig config = new AppConfig(); + config.setVboxManagePath(getConfigValue("vboxManagePath", "VBoxManage")); + config.setPdfExportDirectory(getConfigValue("pdfExportDirectory", + System.getProperty("user.home"))); + return config; } /** * Enregistre la configuration applicative. * - * @param config configuration a persister + * @param config configuration à persister */ public void saveConfig(AppConfig config) { - throw new UnsupportedOperationException("Not implemented yet"); + upsertConfig("vboxManagePath", config.getVboxManagePath()); + upsertConfig("pdfExportDirectory", config.getPdfExportDirectory()); + } + + private String getConfigValue(String key, String defaultValue) { + String sql = "SELECT valeur FROM app_config WHERE cle=?"; + try (PreparedStatement ps = getConnection().prepareStatement(sql)) { + ps.setString(1, key); + ResultSet rs = ps.executeQuery(); + if (rs.next()) return rs.getString("valeur"); + } catch (SQLException e) { + throw new RuntimeException("Erreur getConfigValue : " + e.getMessage(), e); + } + return defaultValue; + } + + private void upsertConfig(String key, String value) { + String sql = "INSERT OR REPLACE INTO app_config (cle, valeur) VALUES (?,?)"; + try (PreparedStatement ps = getConnection().prepareStatement(sql)) { + ps.setString(1, key); + ps.setString(2, value); + ps.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException("Erreur upsertConfig : " + e.getMessage(), e); + } } -} +} \ No newline at end of file diff --git a/src/main/java/tg/cyberlabmanager/pdf/PdfExporter.java b/src/main/java/tg/cyberlabmanager/pdf/PdfExporter.java index 2ffbcb6..7a6679f 100644 --- a/src/main/java/tg/cyberlabmanager/pdf/PdfExporter.java +++ b/src/main/java/tg/cyberlabmanager/pdf/PdfExporter.java @@ -5,36 +5,254 @@ import tg.cyberlabmanager.model.Snapshot; import tg.cyberlabmanager.model.VirtualMachine; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.font.PDType1Font; +import org.apache.pdfbox.pdmodel.font.Standard14Fonts; + import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.List; /** - * Service responsable de l'export d'un laboratoire au format PDF. + * Génère un rapport PDF récapitulatif d'un laboratoire Cyber Lab Manager. + * Le rapport contient : en-tête du labo, liste des VMs, snapshots et journal de bord. + * + * Utilise Apache PDFBox 3.x. */ public class PdfExporter { + + private static final float MARGIN = 50f; + private static final float PAGE_HEIGHT = PDRectangle.A4.getHeight(); + private static final float PAGE_WIDTH = PDRectangle.A4.getWidth(); + private static final float CONTENT_WIDTH = PAGE_WIDTH - 2 * MARGIN; + + private static final float FONT_TITLE = 18f; + private static final float FONT_HEADING = 13f; + private static final float FONT_SUBHEAD = 11f; + private static final float FONT_BODY = 10f; + + private static final PDType1Font FONT_BOLD = new PDType1Font(Standard14Fonts.FontName.HELVETICA_BOLD); + private static final PDType1Font FONT_REGULAR = new PDType1Font(Standard14Fonts.FontName.HELVETICA); + private static final PDType1Font FONT_OBLIQUE = new PDType1Font(Standard14Fonts.FontName.HELVETICA_OBLIQUE); + + private static final DateTimeFormatter DISPLAY_FORMAT = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm"); + + private PDDocument document; + private PDPageContentStream content; + private float cursorY; + /** - * Cree un exporteur PDF. + * Génère et sauvegarde le rapport PDF d'un laboratoire. + * + * @param lab le laboratoire exporté + * @param vms liste des VMs du labo (déjà filtrée par le contrôleur) + * @param snapshots liste des snapshots (déjà filtrée par le contrôleur) + * @param journals liste des entrées de journal (déjà filtrée par le contrôleur) + * @param filePath chemin complet du fichier PDF à créer + * @throws IOException si l'écriture du fichier échoue */ - public PdfExporter() { + public void exportLabToPdf(Lab lab, + List vms, + List snapshots, + List journals, + String filePath) throws IOException { + + document = new PDDocument(); + newPage(); + + // En-tête + writeLine("Rapport – " + nvl(lab.getTitle(), "Sans titre"), FONT_BOLD, FONT_TITLE, true); + writeLine("Catégorie : " + nvl(lab.getCategory(), "Non définie"), FONT_REGULAR, FONT_BODY, false); + writeLine("Généré le : " + now(), FONT_REGULAR, FONT_BODY, false); + if (lab.getDescription() != null && !lab.getDescription().isBlank()) { + writeLine("Description : " + lab.getDescription(), FONT_OBLIQUE, FONT_BODY, false); + } + spacer(12f); + separator(); + spacer(8f); + + // Section 1 : Machines virtuelles + writeLine("1. Machines virtuelles (" + vms.size() + ")", FONT_BOLD, FONT_HEADING, false); + spacer(6f); + + if (vms.isEmpty()) { + writeLine(" Aucune machine virtuelle dans ce laboratoire.", FONT_OBLIQUE, FONT_BODY, false); + } else { + for (VirtualMachine vm : vms) { + checkPageBreak(60f); + writeLine(" ▸ " + nvl(vm.getName(), "Sans nom"), FONT_BOLD, FONT_SUBHEAD, false); + writeLine(" UUID : " + nvl(vm.getUuid(), "—"), FONT_REGULAR, FONT_BODY, false); + writeLine(" Statut : " + (vm.getStatus() != null ? vm.getStatus().name() : "UNKNOWN"), + FONT_REGULAR, FONT_BODY, false); + if (vm.getDocumentationUrl() != null && !vm.getDocumentationUrl().isBlank()) { + writeLine(" Doc : " + vm.getDocumentationUrl(), FONT_REGULAR, FONT_BODY, false); + } + spacer(5f); + } + } + + spacer(8f); + separator(); + spacer(8f); + + // Section 2 : Snapshots + writeLine("2. Snapshots (" + snapshots.size() + ")", FONT_BOLD, FONT_HEADING, false); + spacer(6f); + + if (snapshots.isEmpty()) { + writeLine(" Aucun snapshot enregistré.", FONT_OBLIQUE, FONT_BODY, false); + } else { + for (Snapshot snap : snapshots) { + checkPageBreak(40f); + writeLine(" ▸ " + nvl(snap.getName(), "Sans nom"), FONT_BOLD, FONT_SUBHEAD, false); + writeLine(" Date : " + formatDate(snap.getCreatedAt()), FONT_REGULAR, FONT_BODY, false); + writeLine(" Type : " + (snap.isOnline() ? "À chaud (VM allumée)" : "À froid (VM éteinte)"), + FONT_REGULAR, FONT_BODY, false); + if (snap.getDescription() != null && !snap.getDescription().isBlank()) { + writeLine(" Note : " + snap.getDescription(), FONT_OBLIQUE, FONT_BODY, false); + } + spacer(4f); + } + } + + spacer(8f); + separator(); + spacer(8f); + + // Section 3 : Journal de bord + writeLine("3. Journal de bord (" + journals.size() + " entrée(s))", FONT_BOLD, FONT_HEADING, false); + spacer(6f); + + if (journals.isEmpty()) { + writeLine(" Aucune note dans le journal.", FONT_OBLIQUE, FONT_BODY, false); + } else { + for (JournalEntry entry : journals) { + checkPageBreak(40f); + writeLine(" [" + formatDate(entry.getTimestamp()) + "]", FONT_BOLD, FONT_BODY, false); + writeWrappedText(" " + nvl(entry.getContent(), ""), FONT_REGULAR, FONT_BODY); + spacer(5f); + } + } + + writeFooter(); + + content.close(); + document.save(filePath); + document.close(); + } + + + // Gestion des pages + + + private void newPage() throws IOException { + if (content != null) { + content.close(); + } + PDPage page = new PDPage(PDRectangle.A4); + document.addPage(page); + content = new PDPageContentStream(document, page); + cursorY = PAGE_HEIGHT - MARGIN; + } + + private void checkPageBreak(float needed) throws IOException { + if (cursorY - needed < MARGIN + 20f) { + newPage(); + } + } + + // Écriture de contenu + + private void writeLine(String text, PDType1Font font, float size, boolean underline) + throws IOException { + checkPageBreak(size + 6f); + cursorY -= size + 2f; + + content.beginText(); + content.setFont(font, size); + content.newLineAtOffset(MARGIN, cursorY); + content.showText(sanitize(text)); + content.endText(); + + if (underline) { + float textWidth = font.getStringWidth(sanitize(text)) / 1000 * size; + content.moveTo(MARGIN, cursorY - 2f); + content.lineTo(MARGIN + textWidth, cursorY - 2f); + content.stroke(); + cursorY -= 4f; + } + } + + private void writeWrappedText(String text, PDType1Font font, float size) throws IOException { + float maxWidth = CONTENT_WIDTH; + String[] words = sanitize(text).split(" "); + StringBuilder line = new StringBuilder(); + + for (String word : words) { + String candidate = line.isEmpty() ? word : line + " " + word; + float w = font.getStringWidth(candidate) / 1000 * size; + if (w > maxWidth && !line.isEmpty()) { + writeLine(line.toString(), font, size, false); + line = new StringBuilder(word); + } else { + line = new StringBuilder(candidate); + } + } + if (!line.isEmpty()) { + writeLine(line.toString(), font, size, false); + } + } + + private void spacer(float points) { + cursorY -= points; + } + + private void separator() throws IOException { + content.setLineWidth(0.5f); + content.moveTo(MARGIN, cursorY); + content.lineTo(PAGE_WIDTH - MARGIN, cursorY); + content.stroke(); + cursorY -= 2f; + } + + private void writeFooter() throws IOException { + float footerY = MARGIN - 10f; + content.beginText(); + content.setFont(FONT_OBLIQUE, 8f); + content.newLineAtOffset(MARGIN, footerY); + content.showText("Cyber Lab Manager – Rapport généré le " + now()); + content.endText(); + } + + + // Utilitaires + + private String nvl(String value, String fallback) { + return (value != null && !value.isBlank()) ? value : fallback; + } + + private String now() { + return LocalDateTime.now().format(DISPLAY_FORMAT); + } + + private String formatDate(LocalDateTime dt) { + return dt != null ? dt.format(DISPLAY_FORMAT) : "—"; } /** - * Exporte les donnees d'un laboratoire dans un fichier PDF. - * - * @param lab laboratoire exporte - * @param vms machines virtuelles du laboratoire - * @param snapshots snapshots a inclure - * @param journals notes d'analyse a inclure - * @param filePath chemin du fichier PDF cible - * @throws IOException si l'ecriture du PDF echoue + * Nettoie les caractères non supportés par les polices standard PDF (Latin-1 uniquement). + * Remplace les caractères hors plage par '?'. */ - public void exportLabToPdf( - Lab lab, - List vms, - List snapshots, - List journals, - String filePath - ) throws IOException { - throw new UnsupportedOperationException("Not implemented yet"); - } -} + private String sanitize(String text) { + if (text == null) return ""; + StringBuilder sb = new StringBuilder(); + for (char c : text.toCharArray()) { + sb.append(c <= 255 ? c : '?'); + } + return sb.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/tg/cyberlabmanager/ui/LabListView.java b/src/main/java/tg/cyberlabmanager/ui/LabListView.java index 8e06fb6..cc6ee36 100644 --- a/src/main/java/tg/cyberlabmanager/ui/LabListView.java +++ b/src/main/java/tg/cyberlabmanager/ui/LabListView.java @@ -1,12 +1,58 @@ package tg.cyberlabmanager.ui; +import javafx.geometry.Insets; +import javafx.scene.control.Label; +import javafx.scene.layout.VBox; +import tg.cyberlabmanager.ui.LabListView; + /** - * Vue chargee d'afficher la liste des laboratoires. + * Vue chargee d'afficher la liste des laboratoires et leurs cartes. */ public class LabListView { + + /** + * Cree le composant visuel (VBox) representant la carte d'un laboratoire. + * + * @param name Le nom du laboratoire + * @param badgeText Le texte du theme (Malware, etc.) + * @param badgeStyleClass La classe CSS pour colorer le badge + * @param selected Si le laboratoire est le laboratoire actif + * @return Le VBox pre-configure + */ + public static VBox createLabCard(String name, String badgeText, String badgeStyleClass, boolean selected) { + VBox card = new VBox(5); + card.setPadding(new Insets(10, 12, 10, 12)); + card.getStyleClass().add(selected ? "lab-card-selected" : "lab-card"); + + Label title = new Label(name); + title.getStyleClass().add(selected ? "lab-card-title-selected" : "lab-card-title"); + + Label badge = new Label(badgeText); + badge.getStyleClass().addAll("badge", badgeStyleClass); + + card.getChildren().addAll(title, badge); + return card; + } + /** - * Cree la vue de liste des laboratoires. + * Crée la carte "VMs sans labo" affichée en bas de la sidebar. + * + * @param count nombre de VMs orphelines + * @param selected si la carte est actuellement sélectionnée + * @return le VBox pré-configuré avec les styles orphelin */ - public LabListView() { + public static VBox createOrphanCard(int count, boolean selected) { + VBox card = new VBox(5); + card.setPadding(new Insets(10, 12, 10, 12)); + card.getStyleClass().add(selected ? "lab-card-orphan-selected" : "lab-card-orphan"); + + Label title = new Label("VMs sans labo"); + title.getStyleClass().add("lab-card-title-orphan"); + + Label badge = new Label(count + " VM" + (count > 1 ? "s" : "")); + badge.getStyleClass().addAll("badge", "badge-orphan"); + + card.getChildren().addAll(title, badge); + return card; } } diff --git a/src/main/java/tg/cyberlabmanager/ui/VmTableView.java b/src/main/java/tg/cyberlabmanager/ui/VmTableView.java new file mode 100644 index 0000000..ce05837 --- /dev/null +++ b/src/main/java/tg/cyberlabmanager/ui/VmTableView.java @@ -0,0 +1,53 @@ +package tg.cyberlabmanager.ui; + +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; + +/** + * Vue chargee des elements du tableau des machines virtuelles. + */ +public class VmTableView { + + /** + * Cree la ligne HBox pour afficher une Machine Virtuelle dans le tableau. + * + * @param name Nom de la VM + * @param uuid UUID court + * @param status Statut en texte clair + * @param os OS de la machine + * @return La ligne d'affichage configurée + */ + public static HBox createVmRow(String name, String uuid, String status, String os) { + HBox row = new HBox(); + row.getStyleClass().add("vm-row-selected"); + row.setAlignment(Pos.CENTER_LEFT); + row.setPadding(new Insets(0, 20, 0, 22)); + + Label lName = new Label(name); + lName.setPrefWidth(240); + lName.getStyleClass().add("vm-name"); + + Label lUuid = new Label(uuid); + lUuid.setPrefWidth(210); + lUuid.getStyleClass().add("vm-uuid"); + + Label lStatus = new Label(status); + lStatus.setPrefWidth(150); + String statusCss = switch (status == null ? "" : status.toLowerCase()) { + case "running" -> "status-running"; + case "stopped", "powered_off" -> "status-stopped"; + case "saved" -> "status-saved"; + case "paused" -> "status-paused"; + default -> "status-unknown"; + }; + lStatus.getStyleClass().addAll("status-badge", statusCss); + + Label lOs = new Label(os); + lOs.getStyleClass().add("vm-os"); + + row.getChildren().addAll(lName, lUuid, lStatus, lOs); + return row; + } +} diff --git a/src/main/resources/tg/cyberlabmanager/ui/cyberlab.css b/src/main/resources/tg/cyberlabmanager/ui/cyberlab.css new file mode 100644 index 0000000..967ba7c --- /dev/null +++ b/src/main/resources/tg/cyberlabmanager/ui/cyberlab.css @@ -0,0 +1,710 @@ +/* ═══════════════════════════════════════════════════════════════════════ + CyberLab Manager — Thème Exact selon Spécifications de Maquette + Couleurs: VS Code Dark (#1E1E1E bg, #333333 borders, #007ACC accent) + Polices: Inter (texte général), JetBrains Mono (monospace) + ═══════════════════════════════════════════════════════════════════════ */ + +/* ── ROOT ──────────────────────────────────────────────────────────────── */ +.root { + -fx-background-color: #1E1E1E; + -fx-font-family: "Inter", "Segoe UI", "Helvetica Neue", Arial, sans-serif; + -fx-font-size: 13; +} + +/* ── SCROLL PANE ───────────────────────────────────────────────────────── */ +.scroll-pane { + -fx-background-color: transparent; + -fx-background: transparent; + -fx-border-color: transparent; + -fx-padding: 0; +} + +.scroll-pane>.viewport { + -fx-background-color: transparent; +} + +.scroll-pane .corner { + -fx-background-color: transparent; +} + +.scroll-bar:vertical, +.scroll-bar:horizontal { + -fx-background-color: #1E1E1E; + -fx-pref-width: 6; + -fx-pref-height: 6; +} + +.scroll-bar .thumb { + -fx-background-color: #424242; + -fx-background-radius: 3; +} + +.scroll-bar .increment-button, +.scroll-bar .decrement-button { + -fx-background-color: transparent; + -fx-pref-width: 0; + -fx-pref-height: 0; +} + +/* ── BARRE DE TITRE (Top Header) ───────────────────────────────────────── */ +.title-bar { + -fx-background-color: #1E1E1E; + -fx-border-color: #333333; + -fx-border-width: 0 0 1 0; + -fx-min-height: 48; + -fx-pref-height: 48; +} + +.title-label { + -fx-text-fill: #FFFFFF; + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; + -fx-font-size: 15; + -fx-font-weight: bold; +} + +/* ── PANNEAU GAUCHE (Lab Inventory) ────────────────────────────────────── */ +.left-panel { + -fx-background-color: #1E1E1E; + -fx-border-color: #333333; + -fx-border-width: 0 1 0 0; + -fx-min-width: 180; + -fx-pref-width: 240; + -fx-max-width: 320; +} + +.section-header { + -fx-background-color: #1E1E1E; + -fx-border-color: #333333; + -fx-border-width: 0 0 1 0; + -fx-min-height: 44; + -fx-pref-height: 44; +} + +.section-title { + -fx-text-fill: #9D9D9D; + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; + -fx-font-size: 11; + -fx-font-weight: bold; +} + +/* Cartes de Lab */ +.lab-card { + -fx-background-color: #252526; + -fx-background-radius: 5; + -fx-border-color: transparent; + -fx-border-width: 1; + -fx-border-radius: 5; + -fx-cursor: hand; + -fx-padding: 10 12 10 12; +} + +.lab-card:hover { + -fx-background-color: #2D2D30; +} + +.lab-card-selected { + -fx-background-color: #2D2D30; + -fx-border-color: #007ACC; + -fx-border-width: 1; + -fx-border-radius: 5; + -fx-background-radius: 5; + -fx-padding: 10 12 10 12; +} + +.lab-card-title { + -fx-text-fill: #CCCCCC; + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; + -fx-font-size: 13; + -fx-font-weight: 500; + -fx-wrap-text: true; +} + +.lab-card-title-selected { + -fx-text-fill: #FFFFFF; + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; + -fx-font-size: 13; + -fx-font-weight: 500; + -fx-wrap-text: true; +} + +/* Badges catégorie */ +.badge { + -fx-text-fill: white; + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; + -fx-font-size: 10; + -fx-background-radius: 4; + -fx-border-radius: 4; + -fx-border-width: 1; + -fx-padding: 2 7 2 7; +} + +/* Malware: bg-red-900/50 text-red-300 border-red-800 */ +.badge-malware { + -fx-background-color: rgba(127, 29, 29, 0.6); + -fx-text-fill: #FCA5A5; + -fx-border-color: #991B1B; +} + +/* Pentest: bg-purple-900/50 text-purple-300 border-purple-800 */ +.badge-pentest { + -fx-background-color: rgba(76, 29, 149, 0.6); + -fx-text-fill: #C4B5FD; + -fx-border-color: #6D28D9; +} + +/* Forensics: bg-blue-900/50 text-blue-300 border-blue-800 */ +.badge-forensics { + -fx-background-color: rgba(30, 58, 138, 0.6); + -fx-text-fill: #93C5FD; + -fx-border-color: #1D4ED8; +} + +/* Network: bg-teal-900/50 text-teal-300 border-teal-800 */ +.badge-network { + -fx-background-color: rgba(19, 78, 74, 0.6); + -fx-text-fill: #5EEAD4; + -fx-border-color: #0F766E; +} + +.badge-default { + -fx-background-color: rgba(55, 65, 81, 0.6); + -fx-text-fill: #9CA3AF; + -fx-border-color: #4B5563; +} + +/* Bouton Import VM */ +.btn-import { + -fx-background-color: #252526; + -fx-text-fill: #CCCCCC; + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; + -fx-font-size: 12; + -fx-border-color: #3E3E42; + -fx-border-radius: 5; + -fx-background-radius: 5; + -fx-border-width: 1; + -fx-cursor: hand; + -fx-padding: 8 0 8 0; +} + +.btn-import:hover { + -fx-background-color: #2D2D30; + -fx-border-color: #007ACC; + -fx-text-fill: #FFFFFF; +} + +/* ── PANNEAU CENTRAL (VM Dashboard) ────────────────────────────────────── */ +.center-panel { + -fx-background-color: #1E1E1E; +} + +.lab-title { + -fx-text-fill: #FFFFFF; + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; + -fx-font-size: 18; + -fx-font-weight: bold; +} + +/* Stats bar */ +.stat-label-key { + -fx-text-fill: #9D9D9D; + -fx-font-size: 12; +} + +.stat-total { + -fx-text-fill: #FFFFFF; + -fx-font-size: 12; + -fx-font-weight: bold; +} + +.stat-running { + -fx-text-fill: #4ADE80; + -fx-font-size: 12; + -fx-font-weight: bold; +} + +.stat-snaps { + -fx-text-fill: #60A5FA; + -fx-font-size: 12; + -fx-font-weight: bold; +} + +.stat-sep { + -fx-text-fill: #4B5563; + -fx-font-size: 12; +} + +/* En-têtes colonnes tableau VM */ +.table-header-label { + -fx-text-fill: #9D9D9D; + -fx-font-family: "JetBrains Mono", "Cascadia Code", "Consolas", monospace; + -fx-font-size: 10; + -fx-font-weight: bold; +} + +.table-header-row { + -fx-background-color: #1E1E1E; + -fx-border-color: #333333; + -fx-border-width: 0 0 1 0; + -fx-min-height: 36; + -fx-pref-height: 36; +} + +/* Lignes VM */ +.vm-row { + -fx-background-color: transparent; + -fx-border-color: #333333; + -fx-border-width: 0 0 1 0; + -fx-cursor: hand; + -fx-padding: 12 20 12 20; + -fx-min-height: 46; +} + +.vm-row:hover { + -fx-background-color: #252526; +} + +.vm-row-selected { + -fx-background-color: #2D2D30; + -fx-border-color: #333333; + -fx-border-width: 0 0 1 0; + -fx-padding: 12 20 12 22; + -fx-min-height: 46; +} + +.vm-name { + -fx-text-fill: #FFFFFF; + -fx-font-size: 13; + -fx-font-weight: 500; +} + +.vm-uuid { + -fx-text-fill: #9D9D9D; + -fx-font-family: "JetBrains Mono", "Cascadia Code", "Consolas", monospace; + -fx-font-size: 12; +} + +.vm-os { + -fx-text-fill: #D4D4D4; + -fx-font-size: 12; +} + +/* Badge statut VM */ +.status-badge { + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; + -fx-font-size: 11; + -fx-font-weight: bold; + -fx-background-radius: 4; + -fx-border-radius: 4; + -fx-border-width: 1; + -fx-padding: 2 8 2 8; +} + +/* Running: bg-green-900/50 text-green-300 border-green-700 */ +.status-running { + -fx-text-fill: #86EFAC; + -fx-background-color: rgba(20, 83, 45, 0.6); + -fx-border-color: #15803D; +} + +/* Stopped: bg-red-900/30 text-red-300 border-red-700 */ +.status-stopped { + -fx-text-fill: #FCA5A5; + -fx-background-color: rgba(127, 29, 29, 0.4); + -fx-border-color: #B91C1C; +} + +.status-saved { + -fx-text-fill: #93C5FD; + -fx-background-color: rgba(30, 58, 138, 0.4); + -fx-border-color: #1D4ED8; +} + +.status-paused { + -fx-text-fill: #FCD34D; + -fx-background-color: rgba(120, 53, 15, 0.4); + -fx-border-color: #B45309; +} + +.status-unknown { + -fx-text-fill: #9CA3AF; + -fx-background-color: rgba(55, 65, 81, 0.4); + -fx-border-color: #4B5563; +} + +/* Bouton Export PDF */ +.btn-export-pdf { + -fx-background-color: #3B82F6; + -fx-text-fill: white; + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; + -fx-font-size: 12; + -fx-font-weight: bold; + -fx-border-radius: 5; + -fx-background-radius: 5; + -fx-border-width: 0; + -fx-cursor: hand; + -fx-padding: 7 16 7 16; +} + +.btn-export-pdf:hover { + -fx-background-color: #2563EB; +} + +/* ── PANNEAU DROIT (Orchestration & Intel) ──────────────────────────────── */ +.right-panel { + -fx-background-color: #1E1E1E; + -fx-border-color: #333333; + -fx-border-width: 0 0 0 1; + -fx-min-width: 280; + -fx-pref-width: 360; + -fx-max-width: 460; +} + +.right-section { + -fx-border-color: #333333; + -fx-border-width: 0 0 1 0; + -fx-padding: 14 16 16 16; +} + +.right-section-title { + -fx-text-fill: #9D9D9D; + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; + -fx-font-size: 10; + -fx-font-weight: bold; +} + +.vm-selected-name { + -fx-text-fill: #6E6E6E; + -fx-font-family: "JetBrains Mono", "Cascadia Code", "Consolas", monospace; + -fx-font-size: 11; +} + +.controls-placeholder { + -fx-text-fill: #4B5563; + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; + -fx-font-size: 11; + -fx-wrap-text: true; +} + +/* Boutons STOP, Save State, Take Snapshot */ +/* STOP: bg-red-900/30 text-red-300 border-red-800 */ +.btn-stop { + -fx-background-color: rgba(127, 29, 29, 0.35); + -fx-text-fill: #FCA5A5; + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; + -fx-font-size: 13; + -fx-font-weight: bold; + -fx-border-color: #991B1B; + -fx-border-width: 1; + -fx-border-radius: 5; + -fx-background-radius: 5; + -fx-cursor: hand; + -fx-pref-height: 42; +} + +.btn-stop:hover { + -fx-background-color: rgba(127, 29, 29, 0.6); + -fx-text-fill: #FEE2E2; +} + +/* Start (caché par défaut, visible quand VM est stoppée) */ +.btn-start { + -fx-background-color: rgba(20, 83, 45, 0.4); + -fx-text-fill: #86EFAC; + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; + -fx-font-size: 13; + -fx-font-weight: bold; + -fx-border-color: #15803D; + -fx-border-width: 1; + -fx-border-radius: 5; + -fx-background-radius: 5; + -fx-cursor: hand; + -fx-pref-height: 42; +} + +.btn-start:hover { + -fx-background-color: rgba(20, 83, 45, 0.7); +} + +/* Save State / Take Snapshot: variant="outline" bg-[#252526] border-[#3E3E42] */ +.btn-secondary { + -fx-background-color: #252526; + -fx-text-fill: #FFFFFF; + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; + -fx-font-size: 12; + -fx-border-color: #3E3E42; + -fx-border-width: 1; + -fx-border-radius: 5; + -fx-background-radius: 5; + -fx-cursor: hand; + -fx-pref-height: 38; +} + +.btn-secondary:hover { + -fx-background-color: #2D2D30; + -fx-border-color: #555555; +} + +/* ComboBox snapshot */ +.combo-box { + -fx-background-color: #252526; + -fx-border-color: #3E3E42; + -fx-border-width: 1; + -fx-border-radius: 4; + -fx-background-radius: 4; +} + +.combo-box .list-cell { + -fx-text-fill: #FFFFFF; + -fx-background-color: #252526; + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; + -fx-font-size: 12; +} + +.combo-box-popup .list-view { + -fx-background-color: #252526; + -fx-border-color: #3E3E42; +} + +.combo-box-popup .list-view .list-cell:hover { + -fx-background-color: #2D2D30; +} + +.combo-box .arrow-button { + -fx-background-color: transparent; +} + +.combo-box .arrow { + -fx-background-color: #9D9D9D; +} + +/* Bouton restore */ +.btn-restore { + -fx-background-color: transparent; + -fx-text-fill: #9D9D9D; + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; + -fx-font-size: 12; + -fx-border-color: #3E3E42; + -fx-border-width: 1; + -fx-border-radius: 4; + -fx-background-radius: 4; + -fx-cursor: hand; + -fx-pref-height: 34; +} + +.btn-restore:hover { + -fx-text-fill: #FFFFFF; + -fx-border-color: #555555; +} + +/* TextArea journal — bg-[#0D1117] font-mono text-xs */ +.journal-area { + -fx-control-inner-background: #0D1117; + -fx-background-color: #0D1117; + -fx-text-fill: #D4D4D4; + -fx-font-family: "JetBrains Mono", "Cascadia Code", "Consolas", monospace; + -fx-font-size: 11; + -fx-border-color: #30363D; + -fx-border-width: 1; + -fx-border-radius: 4; + -fx-background-radius: 4; + -fx-highlight-fill: #264F78; +} + +.journal-area .content { + -fx-background-color: #0D1117; +} + +/* TextArea audit */ +.audit-area { + -fx-control-inner-background: #0D1117; + -fx-background-color: #0D1117; + -fx-text-fill: #9D9D9D; + -fx-font-family: "JetBrains Mono", "Cascadia Code", "Consolas", monospace; + -fx-font-size: 10; + -fx-border-color: #30363D; + -fx-border-width: 1; + -fx-border-radius: 4; + -fx-background-radius: 4; +} + +.audit-area .content { + -fx-background-color: #0D1117; +} + +/* TextField saisie journal */ +.journal-input { + -fx-control-inner-background: #1E1E1E; + -fx-background-color: #1E1E1E; + -fx-text-fill: #FFFFFF; + -fx-prompt-text-fill: #6E6E6E; + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; + -fx-font-size: 11; + -fx-border-color: #3E3E42; + -fx-border-width: 1; + -fx-border-radius: 4; + -fx-background-radius: 4; + -fx-padding: 6; +} + +.journal-input:focused { + -fx-border-color: #007ACC; +} + +/* Bouton Add Note — bg-[#3B82F6] */ +.btn-add-note { + -fx-background-color: #3B82F6; + -fx-text-fill: white; + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; + -fx-font-size: 11; + -fx-font-weight: bold; + -fx-border-width: 0; + -fx-border-radius: 4; + -fx-background-radius: 4; + -fx-cursor: hand; + -fx-padding: 0 12 0 12; + -fx-pref-height: 32; +} + +.btn-add-note:hover { + -fx-background-color: #2563EB; +} + +/* Section Audit Logs — bg-[#252526] header bg */ +.audit-section { + -fx-background-color: #1E1E1E; + -fx-border-color: #333333; + -fx-border-width: 1; + -fx-border-radius: 4; + -fx-background-radius: 4; +} + +.audit-header { + -fx-cursor: hand; + -fx-background-color: #252526; + -fx-pref-height: 42; + -fx-min-height: 42; + -fx-background-radius: 4; +} + +.audit-header:hover { + -fx-background-color: #2D2D30; +} + +.audit-title { + -fx-text-fill: #9D9D9D; + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; + -fx-font-size: 10; + -fx-font-weight: bold; +} + +.audit-arrow { + -fx-text-fill: #9D9D9D; + -fx-font-size: 16; +} + +/* ── BARRE DE STATUT — bg-[#007ACC] VS Code Blue ──────────────────────── */ +.status-bar { + -fx-background-color: #007ACC; + -fx-border-color: #005A9E; + -fx-border-width: 1 0 0 0; + -fx-pref-height: 28; + -fx-min-height: 28; +} + +.status-version { + -fx-text-fill: #FFFFFF; + -fx-font-family: "JetBrains Mono", "Cascadia Code", "Consolas", monospace; + -fx-font-size: 11; +} + +.status-connected { + -fx-text-fill: #FFFFFF; + -fx-font-family: "JetBrains Mono", "Cascadia Code", "Consolas", monospace; + -fx-font-size: 11; +} + +.status-dot-ok { + -fx-text-fill: #4ADE80; + -fx-font-size: 11; +} + +.status-dot-error { + -fx-text-fill: #F87171; + -fx-font-size: 11; +} + +/* ── LABEL VIDE (placeholder) ──────────────────────────────────────────── */ +.empty-label { + -fx-text-fill: #6E6E6E; + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; + -fx-font-size: 12; + -fx-wrap-text: true; +} + +/* ── SÉPARATEUR VERTICAL ───────────────────────────────────────────────── */ +.v-separator { + -fx-background-color: #333333; + -fx-pref-width: 1; + -fx-min-width: 1; + -fx-max-width: 1; +} + +/* ── CARTE ORPHELINES (VMs sans labo) ──────────────────────────────────── */ +.lab-card-orphan { + -fx-background-color: #1E1818; + -fx-background-radius: 5; + -fx-border-color: #78350F; + -fx-border-width: 1; + -fx-border-radius: 5; + -fx-cursor: hand; + -fx-padding: 10 12 10 12; +} + +.lab-card-orphan:hover { + -fx-background-color: #2A1F10; + -fx-border-color: #B45309; +} + +.lab-card-orphan-selected { + -fx-background-color: #2A1F10; + -fx-border-color: #F59E0B; + -fx-border-width: 1; + -fx-border-radius: 5; + -fx-background-radius: 5; + -fx-padding: 10 12 10 12; +} + +.lab-card-title-orphan { + -fx-text-fill: #FCD34D; + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; + -fx-font-size: 13; + -fx-font-weight: 500; + -fx-wrap-text: true; +} + +/* Badge orphelines */ +.badge-orphan { + -fx-background-color: rgba(120, 53, 15, 0.6); + -fx-text-fill: #FCD34D; + -fx-border-color: #B45309; +} + +/* ── BOUTON ASSIGNER AU LABO ────────────────────────────────────────────── */ +.btn-assign-lab { + -fx-background-color: rgba(76, 29, 149, 0.35); + -fx-text-fill: #C4B5FD; + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; + -fx-font-size: 12; + -fx-border-color: #6D28D9; + -fx-border-width: 1; + -fx-border-radius: 5; + -fx-background-radius: 5; + -fx-cursor: hand; + -fx-pref-height: 38; +} + +.btn-assign-lab:hover { + -fx-background-color: rgba(76, 29, 149, 0.65); + -fx-text-fill: #EDE9FE; + -fx-border-color: #8B5CF6; +} \ No newline at end of file diff --git a/src/main/resources/tg/cyberlabmanager/ui/main-view.fxml b/src/main/resources/tg/cyberlabmanager/ui/main-view.fxml index 73431e9..25544af 100644 --- a/src/main/resources/tg/cyberlabmanager/ui/main-view.fxml +++ b/src/main/resources/tg/cyberlabmanager/ui/main-view.fxml @@ -1,20 +1,328 @@ - - + + - + + + + + + + + + +
-
+ + - + + + + +
diff --git a/src/test/java/tg/cyberlabmanager/data/DatabaseManagerTest.java b/src/test/java/tg/cyberlabmanager/data/DatabaseManagerTest.java new file mode 100644 index 0000000..9c34f1d --- /dev/null +++ b/src/test/java/tg/cyberlabmanager/data/DatabaseManagerTest.java @@ -0,0 +1,510 @@ +/* + * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license + * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template + */ +package tg.cyberlabmanager.data; + +/** + * + * @author agokoli + */ +import tg.cyberlabmanager.model.*; + +import org.junit.jupiter.api.*; +import static org.junit.jupiter.api.Assertions.*; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * Tests unitaires de DatabaseManager. + * Utilise SQLite en mémoire (:memory:) — aucun fichier créé sur le disque. + * Chaque test repart d'une base vide grâce au @BeforeEach. + */ +class DatabaseManagerTest { + + private DatabaseManager db; + + @BeforeEach + void setUp() { + // Base en mémoire : rapide, isolée, détruite après chaque test + db = new DatabaseManager("jdbc:sqlite::memory:"); + } + + // ========================================================================= + // LABORATOIRES + // ========================================================================= + + @Test + @DisplayName("saveLab : un nouveau labo reçoit un id généré") + void testSaveLabInsert() { + Lab lab = new Lab(); + lab.setTitle("Labo Malware"); + lab.setDescription("Analyse de malware"); + lab.setCategory("malware"); + + db.saveLab(lab); + + assertNotEquals(0, lab.getId(), "L'id doit être assigné après l'INSERT"); + } + + @Test + @DisplayName("getLab : retrouve un labo par son id") + void testGetLab() { + Lab lab = new Lab(); + lab.setTitle("Labo Pentest"); + lab.setCategory("pentest"); + db.saveLab(lab); + + Lab retrieved = db.getLab(lab.getId()); + + assertNotNull(retrieved); + assertEquals("Labo Pentest", retrieved.getTitle()); + assertEquals("pentest", retrieved.getCategory()); + } + + @Test + @DisplayName("getLab : retourne null si l'id n'existe pas") + void testGetLabNotFound() { + Lab result = db.getLab(9999); + assertNull(result); + } + + @Test + @DisplayName("saveLab : met à jour un labo existant") + void testSaveLabUpdate() { + Lab lab = new Lab(); + lab.setTitle("Ancien titre"); + lab.setCategory("reseau"); + db.saveLab(lab); + + lab.setTitle("Nouveau titre"); + db.saveLab(lab); // doit faire un UPDATE + + Lab updated = db.getLab(lab.getId()); + assertEquals("Nouveau titre", updated.getTitle()); + } + + @Test + @DisplayName("getAllLabs : retourne tous les labos") + void testGetAllLabs() { + Lab lab1 = new Lab(); lab1.setTitle("Labo A"); lab1.setCategory("malware"); + Lab lab2 = new Lab(); lab2.setTitle("Labo B"); lab2.setCategory("pentest"); + Lab lab3 = new Lab(); lab3.setTitle("Labo C"); lab3.setCategory("reseau"); + + db.saveLab(lab1); + db.saveLab(lab2); + db.saveLab(lab3); + + List labs = db.getAllLabs(); + assertEquals(3, labs.size()); + } + + @Test + @DisplayName("getAllLabs : retourne une liste vide si aucun labo") + void testGetAllLabsEmpty() { + List labs = db.getAllLabs(); + assertNotNull(labs); + assertTrue(labs.isEmpty()); + } + + @Test + @DisplayName("deleteLab : supprime un labo existant") + void testDeleteLab() { + Lab lab = new Lab(); + lab.setTitle("Labo à supprimer"); + db.saveLab(lab); + + db.deleteLab(lab); + + assertNull(db.getLab(lab.getId())); + assertEquals(0, db.getAllLabs().size()); + } + + @Test + @DisplayName("deleteLab : les VMs rattachées deviennent orphelines") + void testDeleteLabOrphansVMs() { + Lab lab = new Lab(); + lab.setTitle("Labo avec VMs"); + db.saveLab(lab); + + VirtualMachine vm = new VirtualMachine(); + vm.setName("Kali"); + vm.setUuid("uuid-kali"); + db.saveVirtualMachine(vm, lab.getId()); + + db.deleteLab(lab); // les VMs doivent passer en orphelines + + List orphans = db.getOrphanVMs(); + assertEquals(1, orphans.size()); + assertEquals("Kali", orphans.get(0).getName()); + } + + // ========================================================================= + // MACHINES VIRTUELLES + // ========================================================================= + + @Test + @DisplayName("saveVirtualMachine : une nouvelle VM reçoit un id généré") + void testSaveVMInsert() { + Lab lab = new Lab(); lab.setTitle("Labo"); db.saveLab(lab); + + VirtualMachine vm = new VirtualMachine(); + vm.setName("Ubuntu-22"); + vm.setUuid("uuid-ubuntu"); + vm.setStatus(VMStatus.POWERED_OFF); + + db.saveVirtualMachine(vm, lab.getId()); + + assertNotEquals(0, vm.getId(), "L'id doit être assigné après l'INSERT"); + } + + @Test + @DisplayName("saveVirtualMachine : labId null crée une VM orpheline") + void testSaveVMOrpheline() { + VirtualMachine vm = new VirtualMachine(); + vm.setName("VM-Orpheline"); + vm.setUuid("uuid-orphan"); + + db.saveVirtualMachine(vm, null); + + List orphans = db.getOrphanVMs(); + assertEquals(1, orphans.size()); + assertEquals("VM-Orpheline", orphans.get(0).getName()); + } + + @Test + @DisplayName("getVMsForLab : retourne les VMs d'un labo") + void testGetVMsForLab() { + Lab lab = new Lab(); lab.setTitle("Labo"); db.saveLab(lab); + + VirtualMachine vm1 = new VirtualMachine(); vm1.setName("VM1"); vm1.setUuid("uuid-1"); + VirtualMachine vm2 = new VirtualMachine(); vm2.setName("VM2"); vm2.setUuid("uuid-2"); + + db.saveVirtualMachine(vm1, lab.getId()); + db.saveVirtualMachine(vm2, lab.getId()); + + List vms = db.getVMsForLab(lab.getId()); + assertEquals(2, vms.size()); + } + + @Test + @DisplayName("getVMsForLab : ne retourne pas les VMs d'un autre labo") + void testGetVMsForLabIsolation() { + Lab lab1 = new Lab(); lab1.setTitle("Labo 1"); db.saveLab(lab1); + Lab lab2 = new Lab(); lab2.setTitle("Labo 2"); db.saveLab(lab2); + + VirtualMachine vm = new VirtualMachine(); vm.setName("VM du labo 1"); vm.setUuid("uuid-x"); + db.saveVirtualMachine(vm, lab1.getId()); + + List vmsLab2 = db.getVMsForLab(lab2.getId()); + assertTrue(vmsLab2.isEmpty()); + } + + @Test + @DisplayName("getOrphanVMs : retourne uniquement les VMs sans labo") + void testGetOrphanVMs() { + Lab lab = new Lab(); lab.setTitle("Labo"); db.saveLab(lab); + + VirtualMachine vmRattachee = new VirtualMachine(); + vmRattachee.setName("VM rattachée"); vmRattachee.setUuid("uuid-r"); + db.saveVirtualMachine(vmRattachee, lab.getId()); + + VirtualMachine vmOrpheline = new VirtualMachine(); + vmOrpheline.setName("VM orpheline"); vmOrpheline.setUuid("uuid-o"); + db.saveVirtualMachine(vmOrpheline, null); + + List orphans = db.getOrphanVMs(); + assertEquals(1, orphans.size()); + assertEquals("VM orpheline", orphans.get(0).getName()); + } + + @Test + @DisplayName("removeVMFromLab : détache une VM sans la supprimer") + void testRemoveVMFromLab() { + Lab lab = new Lab(); lab.setTitle("Labo"); db.saveLab(lab); + + VirtualMachine vm = new VirtualMachine(); + vm.setName("Kali"); vm.setUuid("uuid-kali"); + db.saveVirtualMachine(vm, lab.getId()); + + db.removeVMFromLab(vm); + + // La VM ne doit plus être dans le labo + assertTrue(db.getVMsForLab(lab.getId()).isEmpty()); + // Mais elle doit être orpheline + assertEquals(1, db.getOrphanVMs().size()); + } + + @Test + @DisplayName("saveVirtualMachine : le statut est bien persisté") + void testSaveVMStatus() { + Lab lab = new Lab(); lab.setTitle("Labo"); db.saveLab(lab); + + VirtualMachine vm = new VirtualMachine(); + vm.setName("VM Running"); vm.setUuid("uuid-run"); + vm.setStatus(VMStatus.RUNNING); + db.saveVirtualMachine(vm, lab.getId()); + + List vms = db.getVMsForLab(lab.getId()); + assertEquals(VMStatus.RUNNING, vms.get(0).getStatus()); + } + + @Test + @DisplayName("saveVirtualMachine : documentationUrl est bien persistée") + void testSaveVMDocUrl() { + Lab lab = new Lab(); lab.setTitle("Labo"); db.saveLab(lab); + + VirtualMachine vm = new VirtualMachine(); + vm.setName("VM Doc"); vm.setUuid("uuid-doc"); + vm.setDocumentationUrl("https://doc.exemple.com"); + db.saveVirtualMachine(vm, lab.getId()); + + List vms = db.getVMsForLab(lab.getId()); + assertEquals("https://doc.exemple.com", vms.get(0).getDocumentationUrl()); + } + + // ========================================================================= + // SNAPSHOTS + // ========================================================================= + + @Test + @DisplayName("saveSnapshot : un nouveau snapshot reçoit un id généré") + void testSaveSnapshot() { + VirtualMachine vm = new VirtualMachine(); + vm.setName("VM"); vm.setUuid("uuid-snap"); + db.saveVirtualMachine(vm, null); + + Snapshot snap = new Snapshot(); + snap.setName("Snapshot initial"); + snap.setDescription("Etat propre"); + snap.setCreatedAt(LocalDateTime.of(2026, 6, 1, 10, 0)); + snap.setOnline(false); + + db.saveSnapshot(snap, vm.getId()); + + assertNotEquals(0, snap.getId()); + } + + @Test + @DisplayName("getSnapshotsForVM : retourne les snapshots dans l'ordre décroissant") + void testGetSnapshotsForVM() { + VirtualMachine vm = new VirtualMachine(); + vm.setName("VM"); vm.setUuid("uuid-snaps"); + db.saveVirtualMachine(vm, null); + + Snapshot s1 = new Snapshot(); s1.setName("Snap 1"); + s1.setCreatedAt(LocalDateTime.of(2026, 1, 1, 0, 0)); + Snapshot s2 = new Snapshot(); s2.setName("Snap 2"); + s2.setCreatedAt(LocalDateTime.of(2026, 6, 1, 0, 0)); + + db.saveSnapshot(s1, vm.getId()); + db.saveSnapshot(s2, vm.getId()); + + List snaps = db.getSnapshotsForVM(vm.getId()); + assertEquals(2, snaps.size()); + // Le plus récent en premier + assertEquals("Snap 2", snaps.get(0).getName()); + } + + @Test + @DisplayName("getSnapshotsForVM : retourne liste vide si aucun snapshot") + void testGetSnapshotsEmpty() { + VirtualMachine vm = new VirtualMachine(); + vm.setName("VM"); vm.setUuid("uuid-no-snap"); + db.saveVirtualMachine(vm, null); + + assertTrue(db.getSnapshotsForVM(vm.getId()).isEmpty()); + } + + @Test + @DisplayName("saveSnapshot : le champ online est bien persisté") + void testSnapshotOnline() { + VirtualMachine vm = new VirtualMachine(); + vm.setName("VM"); vm.setUuid("uuid-online"); + db.saveVirtualMachine(vm, null); + + Snapshot snap = new Snapshot(); + snap.setName("Snap chaud"); + snap.setCreatedAt(LocalDateTime.now()); + snap.setOnline(true); + db.saveSnapshot(snap, vm.getId()); + + List snaps = db.getSnapshotsForVM(vm.getId()); + assertTrue(snaps.get(0).isOnline()); + } + + // ========================================================================= + // JOURNAL DE BORD + // ========================================================================= + + @Test + @DisplayName("addJournalEntry : une nouvelle entrée reçoit un id généré") + void testAddJournalEntry() { + VirtualMachine vm = new VirtualMachine(); + vm.setName("VM"); vm.setUuid("uuid-journal"); + db.saveVirtualMachine(vm, null); + + JournalEntry entry = new JournalEntry(); + entry.setTimestamp(LocalDateTime.now()); + entry.setContent("Analyse en cours — comportement suspect détecté"); + + db.addJournalEntry(entry, vm.getId()); + + assertNotEquals(0, entry.getId()); + } + + @Test + @DisplayName("getJournalEntriesForVM : retourne les entrées de la plus récente à la plus ancienne") + void testGetJournalEntries() { + VirtualMachine vm = new VirtualMachine(); + vm.setName("VM"); vm.setUuid("uuid-j2"); + db.saveVirtualMachine(vm, null); + + JournalEntry e1 = new JournalEntry(); + e1.setTimestamp(LocalDateTime.of(2026, 1, 1, 8, 0)); + e1.setContent("Première note"); + + JournalEntry e2 = new JournalEntry(); + e2.setTimestamp(LocalDateTime.of(2026, 6, 1, 9, 0)); + e2.setContent("Note récente"); + + db.addJournalEntry(e1, vm.getId()); + db.addJournalEntry(e2, vm.getId()); + + List entries = db.getJournalEntriesForVM(vm.getId()); + assertEquals(2, entries.size()); + assertEquals("Note récente", entries.get(0).getContent()); + } + + @Test + @DisplayName("getJournalEntriesForVM : retourne liste vide si aucune note") + void testGetJournalEntriesEmpty() { + VirtualMachine vm = new VirtualMachine(); + vm.setName("VM"); vm.setUuid("uuid-j-empty"); + db.saveVirtualMachine(vm, null); + + assertTrue(db.getJournalEntriesForVM(vm.getId()).isEmpty()); + } + + // ========================================================================= + // AUDIT + // ========================================================================= + + @Test + @DisplayName("addAuditEntry : une entrée d'audit reçoit un id généré") + void testAddAuditEntry() { + VirtualMachine vm = new VirtualMachine(); + vm.setName("VM"); vm.setUuid("uuid-audit"); + db.saveVirtualMachine(vm, null); + + AuditEntry entry = new AuditEntry(); + entry.setTimestamp(LocalDateTime.now()); + entry.setVmId(vm.getId()); + entry.setAction("START"); + entry.setDetails("Démarrage manuel"); + + db.addAuditEntry(entry); + + assertNotEquals(0, entry.getId()); + } + + @Test + @DisplayName("getAuditLogsForVM : retourne les logs d'une VM") + void testGetAuditLogs() { + VirtualMachine vm = new VirtualMachine(); + vm.setName("VM"); vm.setUuid("uuid-audit2"); + db.saveVirtualMachine(vm, null); + + AuditEntry e1 = new AuditEntry(); + e1.setTimestamp(LocalDateTime.now()); + e1.setVmId(vm.getId()); + e1.setAction("START"); + + AuditEntry e2 = new AuditEntry(); + e2.setTimestamp(LocalDateTime.now()); + e2.setVmId(vm.getId()); + e2.setAction("STOP"); + + db.addAuditEntry(e1); + db.addAuditEntry(e2); + + List logs = db.getAuditLogsForVM(vm.getId()); + assertEquals(2, logs.size()); + } + + @Test + @DisplayName("addAuditEntry : vmId null est accepté") + void testAddAuditEntryNullVmId() { + AuditEntry entry = new AuditEntry(); + entry.setTimestamp(LocalDateTime.now()); + entry.setVmId(null); // pas de VM liée + entry.setAction("APP_START"); + entry.setDetails("Démarrage de l'application"); + + assertDoesNotThrow(() -> db.addAuditEntry(entry)); + assertNotEquals(0, entry.getId()); + } + + @Test + @DisplayName("addAuditEntry : labName est bien persisté") + void testAuditEntryLabName() { + VirtualMachine vm = new VirtualMachine(); + vm.setName("VM"); vm.setUuid("uuid-labname"); + db.saveVirtualMachine(vm, null); + + AuditEntry entry = new AuditEntry(); + entry.setTimestamp(LocalDateTime.now()); + entry.setVmId(vm.getId()); + entry.setAction("SNAPSHOT_TAKEN"); + entry.setLabName("Labo Malware"); + + db.addAuditEntry(entry); + + List logs = db.getAuditLogsForVM(vm.getId()); + assertEquals("Labo Malware", logs.get(0).getLabName()); + } + + // ========================================================================= + // CONFIGURATION + // ========================================================================= + + @Test + @DisplayName("getConfig : retourne des valeurs par défaut si rien n'est sauvegardé") + void testGetConfigDefaults() { + AppConfig config = db.getConfig(); + + assertNotNull(config); + assertEquals("VBoxManage", config.getVboxManagePath()); + assertNotNull(config.getPdfExportDirectory()); + } + + @Test + @DisplayName("saveConfig et getConfig : les valeurs sont bien persistées") + void testSaveAndGetConfig() { + AppConfig config = new AppConfig(); + config.setVboxManagePath("/usr/bin/VBoxManage"); + config.setPdfExportDirectory("/home/agokoli/rapports"); + + db.saveConfig(config); + + AppConfig loaded = db.getConfig(); + assertEquals("/usr/bin/VBoxManage", loaded.getVboxManagePath()); + assertEquals("/home/agokoli/rapports", loaded.getPdfExportDirectory()); + } + + @Test + @DisplayName("saveConfig : une deuxième sauvegarde écrase la première") + void testSaveConfigOverwrite() { + AppConfig config1 = new AppConfig(); + config1.setVboxManagePath("/chemin/ancien"); + db.saveConfig(config1); + + AppConfig config2 = new AppConfig(); + config2.setVboxManagePath("/chemin/nouveau"); + db.saveConfig(config2); + + AppConfig loaded = db.getConfig(); + assertEquals("/chemin/nouveau", loaded.getVboxManagePath()); + } +} \ No newline at end of file diff --git a/src/test/java/tg/cyberlabmanager/hypervisor/MockHypervisor.java b/src/test/java/tg/cyberlabmanager/hypervisor/MockHypervisor.java new file mode 100644 index 0000000..c57771e --- /dev/null +++ b/src/test/java/tg/cyberlabmanager/hypervisor/MockHypervisor.java @@ -0,0 +1,67 @@ +package tg.cyberlabmanager.hypervisor; + +import tg.cyberlabmanager.model.AppConfig; +import tg.cyberlabmanager.model.VMDescriptor; +import tg.cyberlabmanager.model.VMStatus; + +import java.util.ArrayList; +import java.util.List; + +/** + * Implémentation factice de IHypervisor (Rôle 1 - Rail A). + * Simule le comportement d'un hyperviseur réel avec une latence réseau, + * pour permettre de tester Start/Stop/Snapshot sans dépendre de Rôle 3. + * + * TODO (Rôle 3) : remplacer par l'implémentation réelle une fois livrée. + */ +public class MockHypervisor implements IHypervisor { + + @Override + public void applyConfig(AppConfig config) { + // Mock: ne fait rien + } + + @Override + public List listVMs() throws HypervisorException { + return new ArrayList<>(); + } + + @Override + public VMStatus getStatus(String uuid) throws HypervisorException { + return VMStatus.POWERED_OFF; + } + + @Override + public void startVM(String uuid) throws HypervisorException { + sleep(1200); + } + + @Override + public void stopVM(String uuid) throws HypervisorException { + sleep(800); + } + + @Override + public void saveState(String uuid) throws HypervisorException { + sleep(600); + } + + @Override + public void takeSnapshot(String uuid, String name, String description) throws HypervisorException { + sleep(500); + } + + @Override + public void restoreSnapshot(String uuid, String snapshotName) throws HypervisorException { + sleep(700); + } + + private void sleep(long millis) throws HypervisorException { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new HypervisorException("Opération interrompue", e); + } + } +} \ No newline at end of file diff --git a/src/test/java/tg/cyberlabmanager/pdf/PdfExporterTest.java b/src/test/java/tg/cyberlabmanager/pdf/PdfExporterTest.java new file mode 100644 index 0000000..ed01e75 --- /dev/null +++ b/src/test/java/tg/cyberlabmanager/pdf/PdfExporterTest.java @@ -0,0 +1,385 @@ +package tg.cyberlabmanager.pdf; + +import tg.cyberlabmanager.model.*; + +import org.junit.jupiter.api.*; +import static org.junit.jupiter.api.Assertions.*; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * Tests unitaires de PdfExporter. + * Chaque test génère un vrai fichier PDF temporaire puis le supprime. + * On ne teste pas le contenu visuel mais les comportements : création, + * taille, gestion des cas limites, robustesse. + */ +class PdfExporterTest { + + private PdfExporter exporter; + private String tempDir; + + @BeforeEach + void setUp() { + exporter = new PdfExporter(); + tempDir = System.getProperty("java.io.tmpdir"); + } + + // Utilitaires + + /** Crée un labo de test avec les champs remplis. */ + private Lab makeLab(String title, String category, String description) { + Lab lab = new Lab(); + lab.setTitle(title); + lab.setCategory(category); + lab.setDescription(description); + return lab; + } + + /** Crée une VM de test. */ + private VirtualMachine makeVM(String name, String uuid, VMStatus status) { + VirtualMachine vm = new VirtualMachine(); + vm.setName(name); + vm.setUuid(uuid); + vm.setStatus(status); + return vm; + } + + /** Crée un snapshot de test. */ + private Snapshot makeSnapshot(String name, boolean online) { + Snapshot s = new Snapshot(); + s.setName(name); + s.setCreatedAt(LocalDateTime.of(2026, 6, 1, 10, 0)); + s.setDescription("Description du snapshot"); + s.setOnline(online); + return s; + } + + /** Crée une entrée de journal de test. */ + private JournalEntry makeJournalEntry(String content) { + JournalEntry e = new JournalEntry(); + e.setTimestamp(LocalDateTime.of(2026, 6, 1, 14, 30)); + e.setContent(content); + return e; + } + + /** Retourne un chemin de fichier PDF temporaire unique. */ + private String tempPdf(String name) { + return tempDir + File.separator + "cyberlab_test_" + name + "_" + + System.currentTimeMillis() + ".pdf"; + } + + /** Supprime un fichier si il existe. */ + private void cleanup(String path) { + File f = new File(path); + if (f.exists()) f.delete(); + } + + // Tests de création de fichier + + @Test + @DisplayName("exportLabToPdf : crée bien un fichier PDF sur le disque") + void testFileIsCreated() throws IOException { + String path = tempPdf("creation"); + try { + Lab lab = makeLab("Labo Malware", "malware", "Test de création"); + exporter.exportLabToPdf(lab, List.of(), List.of(), List.of(), path); + + assertTrue(new File(path).exists(), "Le fichier PDF doit exister"); + } finally { + cleanup(path); + } + } + + @Test + @DisplayName("exportLabToPdf : le fichier PDF n'est pas vide") + void testFileIsNotEmpty() throws IOException { + String path = tempPdf("notempty"); + try { + Lab lab = makeLab("Labo Test", "pentest", null); + exporter.exportLabToPdf(lab, List.of(), List.of(), List.of(), path); + + long size = new File(path).length(); + assertTrue(size > 0, "Le fichier PDF ne doit pas être vide, taille : " + size); + } finally { + cleanup(path); + } + } + + @Test + @DisplayName("exportLabToPdf : le fichier commence par la signature PDF (%PDF)") + void testFileIsPdf() throws IOException { + String path = tempPdf("signature"); + try { + Lab lab = makeLab("Labo Réseau", "réseau", null); + exporter.exportLabToPdf(lab, List.of(), List.of(), List.of(), path); + + byte[] header = Files.readAllBytes(new File(path).toPath()); + // Un PDF valide commence toujours par "%PDF" + String start = new String(header, 0, Math.min(4, header.length)); + assertEquals("%PDF", start, "Le fichier doit commencer par %PDF"); + } finally { + cleanup(path); + } + } + + // Tests avec données réelles + + @Test + @DisplayName("exportLabToPdf : fonctionne avec une VM, un snapshot et une note") + void testWithFullData() throws IOException { + String path = tempPdf("fulldata"); + try { + Lab lab = makeLab("Labo Pentest", "pentest", "Tests d'intrusion réseau"); + + VirtualMachine vm = makeVM("Kali-Linux", "uuid-kali-001", VMStatus.RUNNING); + vm.setDocumentationUrl("https://kali.org"); + + Snapshot snap = makeSnapshot("Etat initial", false); + JournalEntry entry = makeJournalEntry("Scan réseau effectué — ports 22, 80, 443 ouverts"); + + exporter.exportLabToPdf( + lab, + List.of(vm), + List.of(snap), + List.of(entry), + path + ); + + assertTrue(new File(path).exists()); + assertTrue(new File(path).length() > 1000, + "Le PDF avec données doit être plus grand que 1Ko"); + } finally { + cleanup(path); + } + } + + @Test + @DisplayName("exportLabToPdf : fonctionne avec plusieurs VMs") + void testWithMultipleVMs() throws IOException { + String path = tempPdf("multivms"); + try { + Lab lab = makeLab("Labo Multi", "malware", null); + + List vms = List.of( + makeVM("Windows-10", "uuid-win-001", VMStatus.POWERED_OFF), + makeVM("Ubuntu-22", "uuid-ubuntu-001", VMStatus.RUNNING), + makeVM("Kali-2024", "uuid-kali-002", VMStatus.SAVED) + ); + + exporter.exportLabToPdf(lab, vms, List.of(), List.of(), path); + + assertTrue(new File(path).exists()); + } finally { + cleanup(path); + } + } + + @Test + @DisplayName("exportLabToPdf : fonctionne avec plusieurs snapshots") + void testWithMultipleSnapshots() throws IOException { + String path = tempPdf("multisnaps"); + try { + Lab lab = makeLab("Labo Snap", "réseau", null); + VirtualMachine vm = makeVM("VM-Test", "uuid-test", VMStatus.POWERED_OFF); + + List snaps = List.of( + makeSnapshot("Snap initial", false), + makeSnapshot("Snap après config", false), + makeSnapshot("Snap VM allumée", true) + ); + + exporter.exportLabToPdf(lab, List.of(vm), snaps, List.of(), path); + + assertTrue(new File(path).exists()); + } finally { + cleanup(path); + } + } + + @Test + @DisplayName("exportLabToPdf : fonctionne avec plusieurs entrées de journal") + void testWithMultipleJournalEntries() throws IOException { + String path = tempPdf("multijournal"); + try { + Lab lab = makeLab("Labo Journal", "malware", null); + + List entries = List.of( + makeJournalEntry("Première observation : comportement suspect détecté"), + makeJournalEntry("Deuxième observation : connexions réseau anormales vers 192.168.1.100"), + makeJournalEntry("Troisième observation : fichiers chiffrés dans le répertoire Documents") + ); + + exporter.exportLabToPdf(lab, List.of(), List.of(), entries, path); + + assertTrue(new File(path).exists()); + } finally { + cleanup(path); + } + } + + // Tests des cas limites + + @Test + @DisplayName("exportLabToPdf : labo sans description ne plante pas") + void testLabWithoutDescription() throws IOException { + String path = tempPdf("nodesc"); + try { + Lab lab = makeLab("Labo Sans Description", "pentest", null); + assertDoesNotThrow(() -> + exporter.exportLabToPdf(lab, List.of(), List.of(), List.of(), path) + ); + } finally { + cleanup(path); + } + } + + @Test + @DisplayName("exportLabToPdf : VM sans statut ne plante pas") + void testVMWithoutStatus() throws IOException { + String path = tempPdf("nostatus"); + try { + Lab lab = makeLab("Labo", "malware", null); + VirtualMachine vm = new VirtualMachine(); + vm.setName("VM sans statut"); + vm.setUuid("uuid-nostatus"); + // status non défini — reste UNKNOWN par défaut + + assertDoesNotThrow(() -> + exporter.exportLabToPdf(lab, List.of(vm), List.of(), List.of(), path) + ); + } finally { + cleanup(path); + } + } + + @Test + @DisplayName("exportLabToPdf : VM sans documentationUrl ne plante pas") + void testVMWithoutDocUrl() throws IOException { + String path = tempPdf("nodocurl"); + try { + Lab lab = makeLab("Labo", "réseau", null); + VirtualMachine vm = makeVM("VM-Test", "uuid-nodoc", VMStatus.RUNNING); + // documentationUrl non défini + + assertDoesNotThrow(() -> + exporter.exportLabToPdf(lab, List.of(vm), List.of(), List.of(), path) + ); + } finally { + cleanup(path); + } + } + + @Test + @DisplayName("exportLabToPdf : snapshot sans date ne plante pas") + void testSnapshotWithoutDate() throws IOException { + String path = tempPdf("nodate"); + try { + Lab lab = makeLab("Labo", "malware", null); + Snapshot snap = new Snapshot(); + snap.setName("Snap sans date"); + // createdAt null + + assertDoesNotThrow(() -> + exporter.exportLabToPdf(lab, List.of(), List.of(snap), List.of(), path) + ); + } finally { + cleanup(path); + } + } + + @Test + @DisplayName("exportLabToPdf : journal entry sans timestamp ne plante pas") + void testJournalEntryWithoutTimestamp() throws IOException { + String path = tempPdf("notimestamp"); + try { + Lab lab = makeLab("Labo", "pentest", null); + JournalEntry entry = new JournalEntry(); + entry.setContent("Note sans horodatage"); + // timestamp null + + assertDoesNotThrow(() -> + exporter.exportLabToPdf(lab, List.of(), List.of(), List.of(entry), path) + ); + } finally { + cleanup(path); + } + } + + @Test + @DisplayName("exportLabToPdf : note longue est gérée sans planter") + void testLongJournalEntry() throws IOException { + String path = tempPdf("longnote"); + try { + Lab lab = makeLab("Labo", "malware", null); + // Texte très long qui devrait déclencher le wrapping + String longContent = "Analyse détaillée : ".repeat(20) + + "comportement suspect détecté avec connexions réseau anormales " + + "vers plusieurs adresses IP externes non reconnues dans la liste blanche."; + + JournalEntry entry = makeJournalEntry(longContent); + + assertDoesNotThrow(() -> + exporter.exportLabToPdf(lab, List.of(), List.of(), List.of(entry), path) + ); + } finally { + cleanup(path); + } + } + + // Test de saut de page + + @Test + @DisplayName("exportLabToPdf : beaucoup de données génèrent un PDF multi-pages") + void testMultiplePages() throws IOException { + String path = tempPdf("multipages"); + try { + Lab lab = makeLab("Grand Labo", "pentest", "Labo avec beaucoup de contenu"); + + // 10 VMs + List vms = new ArrayList<>(); + for (int i = 1; i <= 10; i++) { + vms.add(makeVM("VM-" + i, "uuid-" + i, VMStatus.POWERED_OFF)); + } + + // 10 snapshots + List snaps = new ArrayList<>(); + for (int i = 1; i <= 10; i++) { + snaps.add(makeSnapshot("Snapshot-" + i, i % 2 == 0)); + } + + // 10 notes de journal + List entries = new ArrayList<>(); + for (int i = 1; i <= 10; i++) { + entries.add(makeJournalEntry( + "Note d'analyse numéro " + i + " : observations détaillées sur le comportement de la VM." + )); + } + + exporter.exportLabToPdf(lab, vms, snaps, entries, path); + + // Un PDF multi-pages est plus grand qu'un PDF d'une page + assertTrue(new File(path).length() > 1000, + "Un PDF avec beaucoup de données doit être suffisamment grand"); + } finally { + cleanup(path); + } + } + + // Test d'erreur + + @Test + @DisplayName("exportLabToPdf : lève IOException si le chemin est invalide") + void testInvalidPathThrowsIOException() { + Lab lab = makeLab("Labo", "malware", null); + String invalidPath = "/chemin/qui/nexiste/pas/rapport.pdf"; + + assertThrows(IOException.class, () -> + exporter.exportLabToPdf(lab, List.of(), List.of(), List.of(), invalidPath) + ); + } +} \ No newline at end of file