diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..b7bbc66 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,25 @@ +## Ce que fait ce MR + + + +## Couche concernée + +- [ ] `model` — entités et POJOs +- [ ] `hypervisor` — IHypervisor / VBoxOrchestrator +- [ ] `data` — DatabaseManager / schema SQL +- [ ] `pdf` — PdfExporter +- [ ] `controller` — LabController +- [ ] `ui` — vues JavaFX +- [ ] Config / build (pom.xml, module-info, CI) + +## Checklist avant review + +- [ ] `./mvnw test` passe en local (0 failure, 0 error) +- [ ] Javadoc présent sur toutes les classes et méthodes publiques ajoutées +- [ ] Pas de `System.out.println` de debug oublié +- [ ] Pas de TODO non intentionnel +- [ ] Les imports inutilisés ont été supprimés (IntelliJ : Ctrl+Alt+O) + +## Pour le reviewer + + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d07d6d8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,34 @@ +name: CI + +on: + push: + branches: [ "main", "develop", "feature/**", "fix/**" ] + pull_request: + branches: [ "develop", "main" ] + +jobs: + build-and-test: + name: Build & Test (Java 17) + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Java 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: maven # met le dépôt Maven en cache → builds 2x plus rapides + + - name: Build & run tests + run: ./mvnw test --no-transfer-progress + + - name: Upload Surefire reports on failure + if: failure() # s'exécute UNIQUEMENT si les tests échouent + uses: actions/upload-artifact@v4 + with: + name: surefire-reports + path: target/surefire-reports/ + retention-days: 7 diff --git a/.gitignore b/.gitignore index eef09ab..a6268c3 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,4 @@ build/ ### Mac OS ### .DS_Store +target/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..baae740 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,74 @@ +# Guide de contribution — Cyber Lab Manager + +## Branches + +| Préfixe | Usage | Exemple | +|---|---|---| +| `feature/` | Nouvelle fonctionnalité | `feature/vbox-orchestrator` | +| `fix/` | Correction de bug | `fix/snapshot-uuid-null` | +| `chore/` | Config, CI, docs | `chore/github-config` | +| `test/` | Ajout ou correction de tests seuls | `test/hypervisor-coverage` | + +**Règle absolue :** on ne pousse jamais directement sur `develop` ou `main`. +Toujours passer par un MR depuis une branche `feature/` ou `fix/`. + +## Format des commits + +On suit [Conventional Commits](https://www.conventionalcommits.org/fr) : + +``` +(): +``` + +| Type | Quand l'utiliser | +|---|---| +| `feat` | Ajout d'une fonctionnalité | +| `fix` | Correction d'un bug | +| `test` | Ajout ou modification de tests | +| `docs` | Documentation (Javadoc, Markdown) | +| `refactor` | Refactoring sans changement de comportement | +| `chore` | Config build, CI, dépendances | + +**Exemples :** +``` +feat(model): add Snapshot uuid and online fields +fix(hypervisor): handle VBoxManage non-zero exit code +test(model): add LabTest unmodifiable list assertion +docs(hypervisor): complete Javadoc on IHypervisor +chore: add GitHub Actions CI workflow +``` + +## Ouvrir un MR + +1. Ta branche est à jour avec `develop` (`git rebase develop` ou `git merge develop`) +2. `./mvnw test` passe en local — **0 failure, 0 error** +3. Javadoc présent sur toutes les classes et méthodes publiques ajoutées +4. Ouvre le MR vers `develop` sur GitHub +5. Remplis le template de MR (pré-rempli automatiquement) +6. Assigne au moins un reviewer + +## Revue de code + +- Le reviewer a **48h** pour donner son retour +- Un commentaire `[bloquant]` doit être résolu avant le merge +- Un commentaire `[suggestion]` est optionnel +- Approuver = confirmer que les tests passent ET que le code est lisible + +## Standards de code + +- **Java 17**, style standard IntelliJ (Ctrl+Alt+L pour formatter) +- Javadoc obligatoire sur toutes les classes et méthodes `public` +- Pas de `System.out.println` → utiliser `java.util.logging.Logger` +- Imports inutilisés supprimés (Ctrl+Alt+O dans IntelliJ) +- Pas de TODO non documenté dans un MR + +## Responsabilités par package + +| Package | Responsable | +|---|---| +| `tg.cyberlabmanager.model` | Rôle 3 | +| `tg.cyberlabmanager.hypervisor` | Rôle 3 | +| `tg.cyberlabmanager.data` | Rôle 2 | +| `tg.cyberlabmanager.pdf` | Rôle 2 | +| `tg.cyberlabmanager.ui` | Rôle 1 | +| `tg.cyberlabmanager.controller` | Rôle 1 | 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/docs/Markdowns/SETUP.md b/docs/Markdowns/SETUP.md new file mode 100644 index 0000000..738100d --- /dev/null +++ b/docs/Markdowns/SETUP.md @@ -0,0 +1,89 @@ +# SETUP — Environnement de développement + +## Prérequis + +| Outil | Version minimale | Vérification | +|---|---|---| +| JDK | 17 (LTS) | `java -version` | +| Maven | 3.8+ | `./mvnw -version` | +| VirtualBox | 7.0+ | Pour les tests d'intégration | +| Git | 2.x | `git --version` | + +> Le projet utilise le Maven Wrapper (`./mvnw`) — pas besoin d'installer Maven globalement. + +--- + +## Configuration IDE — point critique à lire + +Le projet compile en **Java 17** même si tu as un JDK plus récent (21, 22…) sur ta machine. +C'est contrôlé par cette ligne dans `pom.xml` : + +```xml +17 +``` + +Cette propriété fixe simultanément la syntaxe acceptée, le bytecode cible (class file version 61) +et les APIs accessibles — le compilateur de ton JDK s'adapte automatiquement. + +### IntelliJ IDEA + +**File → Project Structure → Project** : +- SDK : ton JDK installé (17, 21, peu importe) +- Language level : **17** + +**File → Settings → Build → Compiler → Java Compiler** : +- Target bytecode version : **17** + +### Pourquoi c'est important + +Si quelqu'un compile avec bytecode Java 21 et pousse du code, les autres obtiendront : +``` +UnsupportedClassVersionError: Unsupported major.minor version 65.0 +``` +La version 65 = Java 21. La version 61 = Java 17 (notre cible). +Garder tout le monde sur la même cible évite ce type de conflit au merge. + +--- + +## Lancer le projet + +```bash +# Compiler et lancer tous les tests +./mvnw test + +# Lancer l'application JavaFX +./mvnw javafx:run + +# Générer la Javadoc dans target/reports/apidocs/ +./mvnw javadoc:javadoc +``` + +> Si tu utilises un JDK non standard, préfixe avec JAVA_HOME : +> `JAVA_HOME=/usr/lib/jvm/jdk-21.0.11-oracle-x64 ./mvnw test` + +--- + +## Structure des branches + +``` +main ← releases stables uniquement +develop ← intégration continue (branche de référence) +feature/x ← développement d'une fonctionnalité +fix/x ← correction de bug +chore/x ← config, CI, documentation +``` + +Voir [CONTRIBUTING.md](../CONTRIBUTING.md) pour les conventions complètes. + +--- + +## Responsabilités par package + +| Package | Rôle | Contenu | +|---|---|---| +| `tg.cyberlabmanager.model` | Rôle 3 | Entités métier (Lab, VM, Snapshot…) | +| `tg.cyberlabmanager.hypervisor` | Rôle 3 | IHypervisor, VBoxOrchestrator | +| `tg.cyberlabmanager.data` | Rôle 2 | DatabaseManager, SQLite | +| `tg.cyberlabmanager.pdf` | Rôle 2 | PdfExporter | +| `tg.cyberlabmanager.ui` | Rôle 1 | Vues JavaFX | +| `tg.cyberlabmanager.controller` | Rôle 1 | LabController | diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 6bcfcc9..74c3ae1 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,3 +1,6 @@ +/** + * Module principal de l'application Cyber Lab Manager. + */ module tg.cyberlabmanager { // ── JavaFX ────────────────────────────────────────────────────────── @@ -23,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; @@ -31,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 70b6866..0f55392 100644 --- a/src/main/java/tg/cyberlabmanager/app/CyberLabApp.java +++ b/src/main/java/tg/cyberlabmanager/app/CyberLabApp.java @@ -4,16 +4,49 @@ import javafx.fxml.FXMLLoader; import javafx.scene.Scene; import javafx.stage.Stage; +import tg.cyberlabmanager.controller.LabController; -import java.io.IOException; - +/** + * Application JavaFX principale de Cyber Lab Manager. + */ public class CyberLabApp extends Application { + + private LabController controller; + + /** + * Cree l'application JavaFX. + */ + public CyberLabApp() { + } + + /** + * Initialise et affiche la fenetre principale JavaFX. + * + * @param stage scene principale fournie par JavaFX + * @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/app/Launcher.java b/src/main/java/tg/cyberlabmanager/app/Launcher.java index 7e25325..113aaca 100644 --- a/src/main/java/tg/cyberlabmanager/app/Launcher.java +++ b/src/main/java/tg/cyberlabmanager/app/Launcher.java @@ -2,7 +2,21 @@ import javafx.application.Application; +/** + * Point d'entree deleguant le lancement a l'application JavaFX. + */ public class Launcher { + /** + * Cree le lanceur de l'application. + */ + public Launcher() { + } + + /** + * Lance l'application JavaFX. + * + * @param args arguments de ligne de commande + */ public static void main(String[] args) { Application.launch(CyberLabApp.class, args); } diff --git a/src/main/java/tg/cyberlabmanager/controller/LabController.java b/src/main/java/tg/cyberlabmanager/controller/LabController.java index 732d284..6074c08 100644 --- a/src/main/java/tg/cyberlabmanager/controller/LabController.java +++ b/src/main/java/tg/cyberlabmanager/controller/LabController.java @@ -1,89 +1,707 @@ 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.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 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(); + private final java.util.Map> vmsByLab = new java.util.HashMap<>(); @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(); + mockLabs(); + initMockVMs(); + 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; + } + + private void initMockVMs() { + for (int labId = 1; labId <= 5; labId++) { + vmsByLab.put(labId, mockVMsForLab(labId)); + } + } + +// ── Affichage de la liste des labos ────────────────────────────────── + private void refreshLabList() { + labListContainer.getChildren().clear(); + + // TODO (Rôle 2) : remplacer mockLabs() par databaseManager.getAllLabs() + List labs = mockLabs(); + + 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) + return vmsByLab.getOrDefault(labId, new ArrayList<>()); + } + + 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) { + } + + public void onExportOrphansClicked(String filePath) { + } + + /** + * Persiste la configuration mise a jour. + * + * @param newConfig nouvelle configuration applicative + */ + public void onConfigUpdated(AppConfig newConfig) { + } + @FXML - private void initialize() { - if (statusLabel != null) { - statusLabel.setText("Cyber Lab Manager pret"); + 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) + ); } - public void onStartVMClicked(VirtualMachine vm) { - throw new UnsupportedOperationException("Not implemented yet"); + @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()); + + // Ajout des traces immédiates (le stub ne fait rien pour l'instant) + appendJournalLine("Export PDF demandé pour " + selectedLab.getTitle()); + appendAuditLine("Export PDF demandé pour " + selectedLab.getTitle()); } - public void onStopVMClicked(VirtualMachine vm) { - throw new UnsupportedOperationException("Not implemented yet"); + @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); + } + ); } - public void onSaveStateClicked(VirtualMachine vm) { - throw new UnsupportedOperationException("Not implemented yet"); + @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); + } + ); } - public void onTakeSnapshotClicked(VirtualMachine vm, String name, String description) { - throw new UnsupportedOperationException("Not implemented yet"); + @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); + } + ); } - public void onRestoreSnapshotClicked(VirtualMachine vm, String snapshotName) { - throw new UnsupportedOperationException("Not implemented yet"); + @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) + ); + }); } - public void onAddJournalEntry(VirtualMachine vm, String text) { - throw new UnsupportedOperationException("Not implemented yet"); + @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) + ); + }); } - public void onImportVMsClicked() { - throw new UnsupportedOperationException("Not implemented yet"); + @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) } - public void onImportConfirm(List selectedVMs, Integer labId) { - throw new UnsupportedOperationException("Not implemented yet"); + @FXML + private void handleToggleAuditLogs() { + auditExpanded = !auditExpanded; + auditArea.setVisible(auditExpanded); + auditArea.setManaged(auditExpanded); + auditArrow.setText(auditExpanded ? "⌄" : "›"); } - public void onExportLabClicked(Lab lab) { - throw new UnsupportedOperationException("Not implemented yet"); + public void shutdown() { + executor.shutdownNow(); } - public void onExportOrphansClicked() { - throw new UnsupportedOperationException("Not implemented yet"); + 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; } - public List getAllLabs() { - return databaseManager.getAllLabs(); + 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); + } } - public List getOrphanVMs() { - return databaseManager.getOrphanVMs(); + 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())); } - public void onConfigUpdated(AppConfig newConfig) { - databaseManager.saveConfig(newConfig); + 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 9cd04a9..833413f 100644 --- a/src/main/java/tg/cyberlabmanager/data/DatabaseManager.java +++ b/src/main/java/tg/cyberlabmanager/data/DatabaseManager.java @@ -9,67 +9,167 @@ import java.util.List; +/** + * Service d'acces aux donnees de l'application. + * + *

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

+ */ public class DatabaseManager { + /** + * Cree un gestionnaire de base de donnees. + */ + public DatabaseManager() { + } + + /** + * Enregistre ou met a jour un laboratoire. + * + * @param lab laboratoire a persister + */ public void saveLab(Lab lab) { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Recupere un laboratoire par son identifiant local. + * + * @param id identifiant local du laboratoire + * @return laboratoire correspondant + */ public Lab getLab(int id) { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Recupere tous les laboratoires. + * + * @return liste des laboratoires + */ public List getAllLabs() { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Supprime un laboratoire sans supprimer les VM physiques. + * + * @param lab laboratoire a supprimer + */ public void deleteLab(Lab lab) { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Enregistre ou met a jour une machine virtuelle. + * + * @param vm machine virtuelle a persister + * @param labId identifiant du laboratoire rattache, ou {@code null} + */ public void saveVirtualMachine(VirtualMachine vm, Integer labId) { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Recupere les machines virtuelles rattachees a un laboratoire. + * + * @param labId identifiant local du laboratoire + * @return liste des machines virtuelles rattachees + */ public List getVMsForLab(int labId) { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Recupere les machines virtuelles sans laboratoire. + * + * @return liste des machines virtuelles orphelines + */ public List getOrphanVMs() { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Detache une machine virtuelle de son laboratoire. + * + * @param vm machine virtuelle a detacher + */ public void removeVMFromLab(VirtualMachine vm) { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Enregistre un snapshot pour une machine virtuelle. + * + * @param snap snapshot a persister + * @param vmId identifiant local de la machine virtuelle + */ public void saveSnapshot(Snapshot snap, int vmId) { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Recupere les snapshots d'une machine virtuelle. + * + * @param vmId identifiant local de la machine virtuelle + * @return liste des snapshots rattaches + */ public List getSnapshotsForVM(int vmId) { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Ajoute une note d'analyse pour une machine virtuelle. + * + * @param entry note d'analyse a persister + * @param vmId identifiant local de la machine virtuelle + */ public void addJournalEntry(JournalEntry entry, int vmId) { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Recupere 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"); } + /** + * Ajoute une entree d'audit. + * + * @param entry entree d'audit a persister + */ public void addAuditEntry(AuditEntry entry) { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Recupere les entrees d'audit d'une machine virtuelle. + * + * @param vmId identifiant local de la machine virtuelle + * @return liste des entrees d'audit + */ public List getAuditLogsForVM(int vmId) { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Recupere la configuration applicative. + * + * @return configuration applicative + */ public AppConfig getConfig() { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Enregistre la configuration applicative. + * + * @param config configuration a persister + */ public void saveConfig(AppConfig config) { throw new UnsupportedOperationException("Not implemented yet"); } diff --git a/src/main/java/tg/cyberlabmanager/hypervisor/HypervisorException.java b/src/main/java/tg/cyberlabmanager/hypervisor/HypervisorException.java index e4c0b3c..d59bf62 100644 --- a/src/main/java/tg/cyberlabmanager/hypervisor/HypervisorException.java +++ b/src/main/java/tg/cyberlabmanager/hypervisor/HypervisorException.java @@ -1,10 +1,24 @@ package tg.cyberlabmanager.hypervisor; +/** + * Exception commune aux erreurs d'orchestration d'hyperviseur. + */ public class HypervisorException extends Exception { + /** + * Cree une exception d'hyperviseur avec un message. + * + * @param message message decrivant l'erreur + */ public HypervisorException(String message) { super(message); } + /** + * Cree une exception d'hyperviseur avec un message et une cause. + * + * @param message message decrivant l'erreur + * @param cause cause initiale de l'erreur + */ public HypervisorException(String message, Throwable cause) { super(message, cause); } diff --git a/src/main/java/tg/cyberlabmanager/hypervisor/IHypervisor.java b/src/main/java/tg/cyberlabmanager/hypervisor/IHypervisor.java index c83757b..99ad3cf 100644 --- a/src/main/java/tg/cyberlabmanager/hypervisor/IHypervisor.java +++ b/src/main/java/tg/cyberlabmanager/hypervisor/IHypervisor.java @@ -4,18 +4,67 @@ import java.util.List; +/** + * Contrat minimal pour piloter un hyperviseur depuis l'application. + */ public interface IHypervisor { + /** + * Liste les machines virtuelles disponibles dans l'hyperviseur. + * + * @return liste de couples ou tableaux de donnees representant les VM + * @throws HypervisorException si la recuperation echoue + */ List listVMs() throws HypervisorException; + /** + * Retourne le statut courant d'une machine virtuelle. + * + * @param uuid UUID de la machine virtuelle + * @return statut courant de la VM + * @throws HypervisorException si le statut ne peut pas etre recupere + */ VMStatus getStatus(String uuid) throws HypervisorException; + /** + * Demarre une machine virtuelle. + * + * @param uuid UUID de la machine virtuelle + * @throws HypervisorException si le demarrage echoue + */ void startVM(String uuid) throws HypervisorException; + /** + * Arrete une machine virtuelle. + * + * @param uuid UUID de la machine virtuelle + * @throws HypervisorException si l'arret echoue + */ void stopVM(String uuid) throws HypervisorException; + /** + * Sauvegarde l'etat d'une machine virtuelle. + * + * @param uuid UUID de la machine virtuelle + * @throws HypervisorException si la sauvegarde d'etat echoue + */ void saveState(String uuid) throws HypervisorException; + /** + * Cree un snapshot pour une machine virtuelle. + * + * @param uuid UUID de la machine virtuelle + * @param name nom du snapshot + * @param description description du snapshot + * @throws HypervisorException si la creation du snapshot echoue + */ void takeSnapshot(String uuid, String name, String description) throws HypervisorException; + /** + * Restaure un snapshot pour une machine virtuelle. + * + * @param uuid UUID de la machine virtuelle + * @param snapshotName nom ou identifiant du snapshot + * @throws HypervisorException si la restauration echoue + */ void restoreSnapshot(String uuid, String snapshotName) throws HypervisorException; } diff --git a/src/main/java/tg/cyberlabmanager/hypervisor/MockHypervisor.java b/src/main/java/tg/cyberlabmanager/hypervisor/MockHypervisor.java new file mode 100644 index 0000000..6fe3480 --- /dev/null +++ b/src/main/java/tg/cyberlabmanager/hypervisor/MockHypervisor.java @@ -0,0 +1,60 @@ +package tg.cyberlabmanager.hypervisor; + +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 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/main/java/tg/cyberlabmanager/hypervisor/VBoxException.java b/src/main/java/tg/cyberlabmanager/hypervisor/VBoxException.java index 7a513e0..9851744 100644 --- a/src/main/java/tg/cyberlabmanager/hypervisor/VBoxException.java +++ b/src/main/java/tg/cyberlabmanager/hypervisor/VBoxException.java @@ -1,19 +1,41 @@ package tg.cyberlabmanager.hypervisor; +/** + * Exception specifique aux commandes VirtualBox executees via VBoxManage. + */ public class VBoxException extends HypervisorException { + /** Code de sortie retourne par VBoxManage. */ private final int exitCode; + /** Sortie standard ou erreur retournee par VBoxManage. */ private final String output; + /** + * Cree une exception VirtualBox avec le code de sortie et la sortie texte. + * + * @param message message decrivant l'erreur + * @param exitCode code de sortie de VBoxManage + * @param output sortie standard ou erreur produite par VBoxManage + */ public VBoxException(String message, int exitCode, String output) { super(message); this.exitCode = exitCode; this.output = output; } + /** + * Retourne le code de sortie de VBoxManage. + * + * @return code de sortie + */ public int getExitCode() { return exitCode; } + /** + * Retourne la sortie produite par VBoxManage. + * + * @return sortie standard ou erreur + */ public String getOutput() { return output; } diff --git a/src/main/java/tg/cyberlabmanager/hypervisor/VBoxOrchestrator.java b/src/main/java/tg/cyberlabmanager/hypervisor/VBoxOrchestrator.java index 4228933..b8f432d 100644 --- a/src/main/java/tg/cyberlabmanager/hypervisor/VBoxOrchestrator.java +++ b/src/main/java/tg/cyberlabmanager/hypervisor/VBoxOrchestrator.java @@ -4,37 +4,53 @@ import java.util.List; +/** + * Implementation de {@link IHypervisor} pour VirtualBox via VBoxManage. + */ public class VBoxOrchestrator implements IHypervisor { + /** + * Cree un orchestrateur VirtualBox. + */ + public VBoxOrchestrator() { + } + + /** {@inheritDoc} */ @Override public List listVMs() throws HypervisorException { throw new UnsupportedOperationException("Not implemented yet"); } + /** {@inheritDoc} */ @Override public VMStatus getStatus(String uuid) throws HypervisorException { throw new UnsupportedOperationException("Not implemented yet"); } + /** {@inheritDoc} */ @Override public void startVM(String uuid) throws HypervisorException { throw new UnsupportedOperationException("Not implemented yet"); } + /** {@inheritDoc} */ @Override public void stopVM(String uuid) throws HypervisorException { throw new UnsupportedOperationException("Not implemented yet"); } + /** {@inheritDoc} */ @Override public void saveState(String uuid) throws HypervisorException { throw new UnsupportedOperationException("Not implemented yet"); } + /** {@inheritDoc} */ @Override public void takeSnapshot(String uuid, String name, String description) throws HypervisorException { throw new UnsupportedOperationException("Not implemented yet"); } + /** {@inheritDoc} */ @Override public void restoreSnapshot(String uuid, String snapshotName) throws HypervisorException { throw new UnsupportedOperationException("Not implemented yet"); diff --git a/src/main/java/tg/cyberlabmanager/model/AppConfig.java b/src/main/java/tg/cyberlabmanager/model/AppConfig.java index 6d6c891..2a3fa9a 100644 --- a/src/main/java/tg/cyberlabmanager/model/AppConfig.java +++ b/src/main/java/tg/cyberlabmanager/model/AppConfig.java @@ -1,21 +1,53 @@ package tg.cyberlabmanager.model; +/** + * Configuration persistante de l'application. + * + *

Elle contient les chemins utilisés par les services techniques, notamment + * VBoxManage et le répertoire d'export PDF.

+ */ public class AppConfig { private String vboxManagePath; private String pdfExportDirectory; + /** + * Crée une configuration vide. + */ + public AppConfig() { + } + + /** + * Retourne le chemin de l'exécutable VBoxManage. + * + * @return chemin configuré vers VBoxManage + */ public String getVboxManagePath() { return vboxManagePath; } + /** + * Définit le chemin de l'exécutable VBoxManage. + * + * @param vboxManagePath chemin vers VBoxManage + */ public void setVboxManagePath(String vboxManagePath) { this.vboxManagePath = vboxManagePath; } + /** + * Retourne le répertoire d'export des rapports PDF. + * + * @return répertoire d'export PDF + */ public String getPdfExportDirectory() { return pdfExportDirectory; } + /** + * Définit le répertoire d'export des rapports PDF. + * + * @param pdfExportDirectory répertoire d'export PDF + */ public void setPdfExportDirectory(String pdfExportDirectory) { this.pdfExportDirectory = pdfExportDirectory; } diff --git a/src/main/java/tg/cyberlabmanager/model/AuditEntry.java b/src/main/java/tg/cyberlabmanager/model/AuditEntry.java index 4c6731f..7c87fea 100644 --- a/src/main/java/tg/cyberlabmanager/model/AuditEntry.java +++ b/src/main/java/tg/cyberlabmanager/model/AuditEntry.java @@ -1,51 +1,158 @@ package tg.cyberlabmanager.model; import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +/** + * Trace automatique d'une action de contrôle effectuée sur une machine virtuelle. + * + *

Cette entité est séparée de {@link VirtualMachine} afin de conserver un + * historique potentiellement volumineux sans alourdir le modèle de la VM.

+ */ public class AuditEntry { + private static final DateTimeFormatter LOG_TIMESTAMP_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME; + private int id; private LocalDateTime timestamp; private Integer vmId; private String action; private String details; + private String labName; + + /** + * Crée une entrée d'audit vide. + */ + public AuditEntry() { + } + /** + * Retourne l'identifiant local de l'entrée d'audit. + * + * @return identifiant local + */ public int getId() { return id; } + /** + * Définit l'identifiant local de l'entrée d'audit. + * + * @param id identifiant local + */ public void setId(int id) { this.id = id; } + /** + * Retourne l'horodatage de l'action. + * + * @return date et heure de l'action + */ public LocalDateTime getTimestamp() { return timestamp; } + /** + * Définit l'horodatage de l'action. + * + * @param timestamp date et heure de l'action + */ public void setTimestamp(LocalDateTime timestamp) { this.timestamp = timestamp; } + /** + * Retourne l'identifiant local de la VM concernée. + * + * @return identifiant de la VM, ou {@code null} si aucune VM n'est liée + */ public Integer getVmId() { return vmId; } + /** + * Définit l'identifiant local de la VM concernée. + * + * @param vmId identifiant de la VM, ou {@code null} + */ public void setVmId(Integer vmId) { this.vmId = vmId; } + /** + * Retourne l'action auditée. + * + * @return action auditée + */ public String getAction() { return action; } + /** + * Définit l'action auditée. + * + * @param action action auditée + */ public void setAction(String action) { this.action = action; } + /** + * Retourne les détails de l'action. + * + * @return détails de l'action + */ public String getDetails() { return details; } + /** + * Définit les détails de l'action. + * + * @param details détails de l'action + */ public void setDetails(String details) { this.details = details; } + + /** + * Retourne le nom du laboratoire concerné. + * + * @return nom du laboratoire + */ + public String getLabName() { + return labName; + } + + /** + * Définit le nom du laboratoire concerné. + * + * @param labName nom du laboratoire + */ + public void setLabName(String labName) { + this.labName = labName; + } + + /** + * Construit une représentation textuelle de l'entrée d'audit. + * + * @return ligne de log lisible + */ + public String getLog() { + StringBuilder log = new StringBuilder(); + log.append(timestamp == null ? "unknown-time" : timestamp.format(LOG_TIMESTAMP_FORMATTER)); + log.append(" | action=").append(action == null ? "UNKNOWN" : action); + + if (vmId != null) { + log.append(" | vmId=").append(vmId); + } + if (labName != null && !labName.isBlank()) { + log.append(" | lab=").append(labName); + } + if (details != null && !details.isBlank()) { + log.append(" | details=").append(details); + } + + return log.toString(); + } } diff --git a/src/main/java/tg/cyberlabmanager/model/JournalEntry.java b/src/main/java/tg/cyberlabmanager/model/JournalEntry.java index ec7031f..d2287ad 100644 --- a/src/main/java/tg/cyberlabmanager/model/JournalEntry.java +++ b/src/main/java/tg/cyberlabmanager/model/JournalEntry.java @@ -2,31 +2,73 @@ import java.time.LocalDateTime; +/** + * Note d'analyse horodatée saisie par l'analyste pour une machine virtuelle. + * + *

Une entrée de journal est rattachée au cycle de vie logique d'une + * {@link VirtualMachine}.

+ */ public class JournalEntry { private int id; private LocalDateTime timestamp; private String content; + /** + * Crée une entrée de journal vide. + */ + public JournalEntry() { + } + + /** + * Retourne l'identifiant local de l'entrée de journal. + * + * @return identifiant local + */ public int getId() { return id; } + /** + * Définit l'identifiant local de l'entrée de journal. + * + * @param id identifiant local + */ public void setId(int id) { this.id = id; } + /** + * Retourne l'horodatage de la note. + * + * @return date et heure de la note + */ public LocalDateTime getTimestamp() { return timestamp; } + /** + * Définit l'horodatage de la note. + * + * @param timestamp date et heure de la note + */ public void setTimestamp(LocalDateTime timestamp) { this.timestamp = timestamp; } + /** + * Retourne le contenu de la note. + * + * @return contenu saisi par l'analyste + */ public String getContent() { return content; } + /** + * Définit le contenu de la note. + * + * @param content contenu saisi par l'analyste + */ public void setContent(String content) { this.content = content; } diff --git a/src/main/java/tg/cyberlabmanager/model/Lab.java b/src/main/java/tg/cyberlabmanager/model/Lab.java index 9e02ad8..8f82900 100644 --- a/src/main/java/tg/cyberlabmanager/model/Lab.java +++ b/src/main/java/tg/cyberlabmanager/model/Lab.java @@ -4,6 +4,13 @@ import java.util.Collections; import java.util.List; +/** + * Entité racine représentant un laboratoire thématique. + * + *

Ses attributs correspondent aux colonnes de la table {@code lab}. Les VM + * rattachées sont exposées en lecture seule et modifiées via les méthodes + * d'agrégation.

+ */ public class Lab { private int id; private String title; @@ -11,9 +18,20 @@ public class Lab { private String category; private final List virtualMachines = new ArrayList<>(); + /** + * Crée un laboratoire vide. + */ public Lab() { } + /** + * Crée un laboratoire avec ses champs principaux. + * + * @param id identifiant local du laboratoire + * @param title titre du laboratoire + * @param description description du laboratoire + * @param category catégorie du laboratoire + */ public Lab(int id, String title, String description, String category) { this.id = id; this.title = title; @@ -21,46 +39,101 @@ public Lab(int id, String title, String description, String category) { this.category = category; } + /** + * Retourne l'identifiant local du laboratoire. + * + * @return identifiant local + */ public int getId() { return id; } + /** + * Définit l'identifiant local du laboratoire. + * + * @param id identifiant local + */ public void setId(int id) { this.id = id; } + /** + * Retourne le titre du laboratoire. + * + * @return titre du laboratoire + */ public String getTitle() { return title; } + /** + * Définit le titre du laboratoire. + * + * @param title titre du laboratoire + */ public void setTitle(String title) { this.title = title; } + /** + * Retourne la description du laboratoire. + * + * @return description du laboratoire + */ public String getDescription() { return description; } + /** + * Définit la description du laboratoire. + * + * @param description description du laboratoire + */ public void setDescription(String description) { this.description = description; } + /** + * Retourne la catégorie du laboratoire. + * + * @return catégorie du laboratoire + */ public String getCategory() { return category; } + /** + * Définit la catégorie du laboratoire. + * + * @param category catégorie du laboratoire + */ public void setCategory(String category) { this.category = category; } + /** + * Retourne les machines virtuelles rattachées au laboratoire. + * + * @return liste non modifiable des machines virtuelles + */ public List getVirtualMachines() { return Collections.unmodifiableList(virtualMachines); } + /** + * Rattache une machine virtuelle au laboratoire. + * + * @param virtualMachine machine virtuelle à ajouter + */ public void addVirtualMachine(VirtualMachine virtualMachine) { virtualMachines.add(virtualMachine); } + /** + * Retire une machine virtuelle du laboratoire. + * + * @param virtualMachine machine virtuelle à retirer + */ public void removeVirtualMachine(VirtualMachine virtualMachine) { virtualMachines.remove(virtualMachine); } diff --git a/src/main/java/tg/cyberlabmanager/model/Snapshot.java b/src/main/java/tg/cyberlabmanager/model/Snapshot.java index bd5e879..cf69b33 100644 --- a/src/main/java/tg/cyberlabmanager/model/Snapshot.java +++ b/src/main/java/tg/cyberlabmanager/model/Snapshot.java @@ -2,41 +2,132 @@ import java.time.LocalDateTime; +/** + * Instantané VirtualBox rattaché à une machine virtuelle. + * + *

Le modèle conserve l'identité locale en base, l'UUID VirtualBox, les + * métadonnées d'affichage et l'information indiquant si l'instantané a été pris + * pendant que la VM était en exécution.

+ */ public class Snapshot { private int id; + private String uuid; private String name; private LocalDateTime createdAt; private String description; + private boolean online; + /** + * Crée un snapshot vide. + */ + public Snapshot() { + } + + /** + * Retourne l'identifiant local du snapshot. + * + * @return identifiant local + */ public int getId() { return id; } + /** + * Définit l'identifiant local du snapshot. + * + * @param id identifiant local + */ public void setId(int id) { this.id = id; } + /** + * Retourne l'UUID VirtualBox du snapshot. + * + * @return UUID VirtualBox + */ + public String getUuid() { + return uuid; + } + + /** + * Définit l'UUID VirtualBox du snapshot. + * + * @param uuid UUID VirtualBox + */ + public void setUuid(String uuid) { + this.uuid = uuid; + } + + /** + * Retourne le nom du snapshot. + * + * @return nom du snapshot + */ public String getName() { return name; } + /** + * Définit le nom du snapshot. + * + * @param name nom du snapshot + */ public void setName(String name) { this.name = name; } + /** + * Retourne la date de création du snapshot. + * + * @return date de création + */ public LocalDateTime getCreatedAt() { return createdAt; } + /** + * Définit la date de création du snapshot. + * + * @param createdAt date de création + */ public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } + /** + * Retourne la description du snapshot. + * + * @return description du snapshot + */ public String getDescription() { return description; } + /** + * Définit la description du snapshot. + * + * @param description description du snapshot + */ public void setDescription(String description) { this.description = description; } + + /** + * Indique si le snapshot a été pris VM allumée. + * + * @return {@code true} si le snapshot inclut l'état en exécution + */ + public boolean isOnline() { + return online; + } + + /** + * Définit si le snapshot a été pris VM allumée. + * + * @param online {@code true} si le snapshot inclut l'état en exécution + */ + public void setOnline(boolean online) { + this.online = online; + } } diff --git a/src/main/java/tg/cyberlabmanager/model/VMStatus.java b/src/main/java/tg/cyberlabmanager/model/VMStatus.java index b6a8744..3b0130a 100644 --- a/src/main/java/tg/cyberlabmanager/model/VMStatus.java +++ b/src/main/java/tg/cyberlabmanager/model/VMStatus.java @@ -1,9 +1,17 @@ package tg.cyberlabmanager.model; +/** + * États stables d'une machine virtuelle tels qu'exposés par l'hyperviseur. + */ public enum VMStatus { + /** La VM est en cours d'exécution. */ RUNNING, + /** La VM est éteinte. */ POWERED_OFF, + /** La VM est dans un état sauvegardé. */ SAVED, + /** La VM est en pause. */ PAUSED, + /** Le statut de la VM n'a pas pu être déterminé. */ UNKNOWN } diff --git a/src/main/java/tg/cyberlabmanager/model/VirtualMachine.java b/src/main/java/tg/cyberlabmanager/model/VirtualMachine.java index f031a89..e8442cd 100644 --- a/src/main/java/tg/cyberlabmanager/model/VirtualMachine.java +++ b/src/main/java/tg/cyberlabmanager/model/VirtualMachine.java @@ -4,6 +4,13 @@ import java.util.Collections; import java.util.List; +/** + * Machine virtuelle importée depuis VirtualBox. + * + *

L'UUID VirtualBox fournit une référence stable. Les snapshots et notes + * d'analyse sont encapsulés pour éviter l'exposition directe des listes + * internes.

+ */ public class VirtualMachine { private int id; private String name; @@ -13,59 +20,135 @@ public class VirtualMachine { private final List snapshots = new ArrayList<>(); private final List journalEntries = new ArrayList<>(); + /** + * Crée une machine virtuelle vide. + */ + public VirtualMachine() { + } + + /** + * Retourne l'identifiant local de la VM. + * + * @return identifiant local + */ public int getId() { return id; } + /** + * Définit l'identifiant local de la VM. + * + * @param id identifiant local + */ public void setId(int id) { this.id = id; } + /** + * Retourne le nom de la VM. + * + * @return nom de la VM + */ public String getName() { return name; } + /** + * Définit le nom de la VM. + * + * @param name nom de la VM + */ public void setName(String name) { this.name = name; } + /** + * Retourne l'UUID VirtualBox de la VM. + * + * @return UUID VirtualBox + */ public String getUuid() { return uuid; } + /** + * Définit l'UUID VirtualBox de la VM. + * + * @param uuid UUID VirtualBox + */ public void setUuid(String uuid) { this.uuid = uuid; } + /** + * Retourne l'URL de documentation associée à la VM. + * + * @return URL de documentation + */ public String getDocumentationUrl() { return documentationUrl; } + /** + * Définit l'URL de documentation associée à la VM. + * + * @param documentationUrl URL de documentation + */ public void setDocumentationUrl(String documentationUrl) { this.documentationUrl = documentationUrl; } + /** + * Retourne le statut courant de la VM. + * + * @return statut de la VM + */ public VMStatus getStatus() { return status; } + /** + * Définit le statut courant de la VM. + * + * @param status statut de la VM + */ public void setStatus(VMStatus status) { this.status = status; } + /** + * Retourne les snapshots rattachés à la VM. + * + * @return liste non modifiable des snapshots + */ public List getSnapshots() { return Collections.unmodifiableList(snapshots); } + /** + * Ajoute un snapshot à la VM. + * + * @param snapshot snapshot à ajouter + */ public void addSnapshot(Snapshot snapshot) { snapshots.add(snapshot); } + /** + * Retourne les notes d'analyse rattachées à la VM. + * + * @return liste non modifiable des notes d'analyse + */ public List getJournalEntries() { return Collections.unmodifiableList(journalEntries); } - public void addJournalEntry(JournalEntry journalEntry) { + /** + * Ajoute une note d'analyse à la VM. + * + * @param journalEntry note d'analyse à ajouter + */ + public void addJournalEntry(JournalEntry journalEntry) { journalEntries.add(journalEntry); } } diff --git a/src/main/java/tg/cyberlabmanager/pdf/PdfExporter.java b/src/main/java/tg/cyberlabmanager/pdf/PdfExporter.java index 5c670d7..2ffbcb6 100644 --- a/src/main/java/tg/cyberlabmanager/pdf/PdfExporter.java +++ b/src/main/java/tg/cyberlabmanager/pdf/PdfExporter.java @@ -8,7 +8,26 @@ import java.io.IOException; import java.util.List; +/** + * Service responsable de l'export d'un laboratoire au format PDF. + */ public class PdfExporter { + /** + * Cree un exporteur PDF. + */ + public PdfExporter() { + } + + /** + * 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 + */ public void exportLabToPdf( Lab lab, List vms, diff --git a/src/main/java/tg/cyberlabmanager/ui/LabListView.java b/src/main/java/tg/cyberlabmanager/ui/LabListView.java index 7a10e42..930ea8f 100644 --- a/src/main/java/tg/cyberlabmanager/ui/LabListView.java +++ b/src/main/java/tg/cyberlabmanager/ui/LabListView.java @@ -1,4 +1,35 @@ package tg.cyberlabmanager.ui; +import javafx.geometry.Insets; +import javafx.scene.control.Label; +import javafx.scene.layout.VBox; + +/** + * 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; + } } diff --git a/src/main/java/tg/cyberlabmanager/ui/MainView.java b/src/main/java/tg/cyberlabmanager/ui/MainView.java index 0e763a1..b38c414 100644 --- a/src/main/java/tg/cyberlabmanager/ui/MainView.java +++ b/src/main/java/tg/cyberlabmanager/ui/MainView.java @@ -1,4 +1,12 @@ package tg.cyberlabmanager.ui; +/** + * Vue principale de l'application. + */ public class MainView { + /** + * Cree la vue principale. + */ + public MainView() { + } } diff --git a/src/main/java/tg/cyberlabmanager/ui/VmControlPanel.java b/src/main/java/tg/cyberlabmanager/ui/VmControlPanel.java index 49b4eea..e0692bc 100644 --- a/src/main/java/tg/cyberlabmanager/ui/VmControlPanel.java +++ b/src/main/java/tg/cyberlabmanager/ui/VmControlPanel.java @@ -1,4 +1,12 @@ package tg.cyberlabmanager.ui; +/** + * Vue chargee des controles utilisateur pour les machines virtuelles. + */ public class VmControlPanel { + /** + * Cree le panneau de controle des machines virtuelles. + */ + public VmControlPanel() { + } } 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..cd0dcce --- /dev/null +++ b/src/main/java/tg/cyberlabmanager/ui/VmTableView.java @@ -0,0 +1,46 @@ +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); + lStatus.getStyleClass().addAll("status-badge", "status-running"); + + 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/data/schema.sql b/src/main/resources/tg/cyberlabmanager/data/schema.sql index fde532e..96160eb 100644 --- a/src/main/resources/tg/cyberlabmanager/data/schema.sql +++ b/src/main/resources/tg/cyberlabmanager/data/schema.sql @@ -35,6 +35,7 @@ CREATE TABLE IF NOT EXISTS audit_entry ( id_audit INTEGER PRIMARY KEY AUTOINCREMENT, timestamp TEXT NOT NULL, id_vm INTEGER, + lab_name TEXT, action TEXT NOT NULL, details TEXT, FOREIGN KEY (id_vm) REFERENCES virtual_machine(id_vm) ON DELETE SET NULL 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..2651f8a --- /dev/null +++ b/src/main/resources/tg/cyberlabmanager/ui/cyberlab.css @@ -0,0 +1,650 @@ +/* ═══════════════════════════════════════════════════════════════════════ + 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; +} \ 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..2f307af 100644 --- a/src/main/resources/tg/cyberlabmanager/ui/main-view.fxml +++ b/src/main/resources/tg/cyberlabmanager/ui/main-view.fxml @@ -1,20 +1,314 @@ - - + + - + + + + + + + + + +
-
+ + - + + + + +
diff --git a/src/test/java/tg/cyberlabmanager/model/.gitkeep b/src/test/java/tg/cyberlabmanager/model/.gitkeep deleted file mode 100644 index 8b13789..0000000 --- a/src/test/java/tg/cyberlabmanager/model/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/test/java/tg/cyberlabmanager/model/AppConfigTest.java b/src/test/java/tg/cyberlabmanager/model/AppConfigTest.java new file mode 100644 index 0000000..5b24c55 --- /dev/null +++ b/src/test/java/tg/cyberlabmanager/model/AppConfigTest.java @@ -0,0 +1,18 @@ +package tg.cyberlabmanager.model; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class AppConfigTest { + @Test + void shouldStoreConfigurationPaths() { + AppConfig config = new AppConfig(); + + config.setVboxManagePath("/usr/bin/VBoxManage"); + config.setPdfExportDirectory("/tmp/reports"); + + assertEquals("/usr/bin/VBoxManage", config.getVboxManagePath()); + assertEquals("/tmp/reports", config.getPdfExportDirectory()); + } +} diff --git a/src/test/java/tg/cyberlabmanager/model/AuditEntryTest.java b/src/test/java/tg/cyberlabmanager/model/AuditEntryTest.java new file mode 100644 index 0000000..493d86a --- /dev/null +++ b/src/test/java/tg/cyberlabmanager/model/AuditEntryTest.java @@ -0,0 +1,51 @@ +package tg.cyberlabmanager.model; + +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class AuditEntryTest { + @Test + void shouldStoreAuditFields() { + LocalDateTime timestamp = LocalDateTime.of(2026, 5, 29, 20, 30); + AuditEntry entry = new AuditEntry(); + + entry.setId(7); + entry.setTimestamp(timestamp); + entry.setVmId(12); + entry.setAction("START"); + entry.setDetails("VM started from UI"); + entry.setLabName("Malware Lab"); + + assertEquals(7, entry.getId()); + assertEquals(timestamp, entry.getTimestamp()); + assertEquals(12, entry.getVmId()); + assertEquals("START", entry.getAction()); + assertEquals("VM started from UI", entry.getDetails()); + assertEquals("Malware Lab", entry.getLabName()); + } + + @Test + void shouldBuildReadableLogLine() { + AuditEntry entry = new AuditEntry(); + entry.setTimestamp(LocalDateTime.of(2026, 5, 29, 20, 30)); + entry.setVmId(12); + entry.setAction("SNAPSHOT_TAKEN"); + entry.setLabName("Pentest Lab"); + entry.setDetails("Snapshot baseline created"); + + assertEquals( + "2026-05-29T20:30:00 | action=SNAPSHOT_TAKEN | vmId=12 | lab=Pentest Lab | details=Snapshot baseline created", + entry.getLog() + ); + } + + @Test + void shouldBuildFallbackLogWhenOptionalFieldsAreMissing() { + AuditEntry entry = new AuditEntry(); + + assertEquals("unknown-time | action=UNKNOWN", entry.getLog()); + } +} diff --git a/src/test/java/tg/cyberlabmanager/model/JournalEntryTest.java b/src/test/java/tg/cyberlabmanager/model/JournalEntryTest.java new file mode 100644 index 0000000..1abd563 --- /dev/null +++ b/src/test/java/tg/cyberlabmanager/model/JournalEntryTest.java @@ -0,0 +1,23 @@ +package tg.cyberlabmanager.model; + +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class JournalEntryTest { + @Test + void shouldStoreJournalEntryFields() { + LocalDateTime timestamp = LocalDateTime.of(2026, 5, 29, 21, 15); + JournalEntry entry = new JournalEntry(); + + entry.setId(3); + entry.setTimestamp(timestamp); + entry.setContent("Observed suspicious network traffic."); + + assertEquals(3, entry.getId()); + assertEquals(timestamp, entry.getTimestamp()); + assertEquals("Observed suspicious network traffic.", entry.getContent()); + } +} diff --git a/src/test/java/tg/cyberlabmanager/model/LabTest.java b/src/test/java/tg/cyberlabmanager/model/LabTest.java new file mode 100644 index 0000000..e8d5bc8 --- /dev/null +++ b/src/test/java/tg/cyberlabmanager/model/LabTest.java @@ -0,0 +1,64 @@ +package tg.cyberlabmanager.model; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class LabTest { + @Test + void shouldCreateLabWithConstructorValues() { + Lab lab = new Lab(1, "Malware Analysis", "Static and dynamic malware lab", "malware"); + + assertEquals(1, lab.getId()); + assertEquals("Malware Analysis", lab.getTitle()); + assertEquals("Static and dynamic malware lab", lab.getDescription()); + assertEquals("malware", lab.getCategory()); + } + + @Test + void shouldUpdateLabFieldsWithSetters() { + Lab lab = new Lab(); + + lab.setId(2); + lab.setTitle("Pentest"); + lab.setDescription("Web attack lab"); + lab.setCategory("pentest"); + + assertEquals(2, lab.getId()); + assertEquals("Pentest", lab.getTitle()); + assertEquals("Web attack lab", lab.getDescription()); + assertEquals("pentest", lab.getCategory()); + } + + @Test + void shouldAddAndRemoveVirtualMachines() { + Lab lab = new Lab(); + VirtualMachine vm = new VirtualMachine(); + vm.setName("Kali"); + + lab.addVirtualMachine(vm); + + List virtualMachines = lab.getVirtualMachines(); + assertEquals(1, virtualMachines.size()); + assertSame(vm, virtualMachines.get(0)); + + lab.removeVirtualMachine(vm); + + assertTrue(lab.getVirtualMachines().isEmpty()); + } + + @Test + void shouldExposeUnmodifiableVirtualMachineList() { + Lab lab = new Lab(); + + assertThrows( + UnsupportedOperationException.class, + () -> lab.getVirtualMachines().add(new VirtualMachine()) + ); + } +} diff --git a/src/test/java/tg/cyberlabmanager/model/SnapshotTest.java b/src/test/java/tg/cyberlabmanager/model/SnapshotTest.java new file mode 100644 index 0000000..9501f63 --- /dev/null +++ b/src/test/java/tg/cyberlabmanager/model/SnapshotTest.java @@ -0,0 +1,30 @@ +package tg.cyberlabmanager.model; + +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SnapshotTest { + @Test + void shouldStoreSnapshotFields() { + LocalDateTime createdAt = LocalDateTime.of(2026, 5, 29, 22, 0); + Snapshot snapshot = new Snapshot(); + + snapshot.setId(4); + snapshot.setUuid("snap-uuid-123"); + snapshot.setName("baseline"); + snapshot.setCreatedAt(createdAt); + snapshot.setDescription("Clean VM state"); + snapshot.setOnline(true); + + assertEquals(4, snapshot.getId()); + assertEquals("snap-uuid-123", snapshot.getUuid()); + assertEquals("baseline", snapshot.getName()); + assertEquals(createdAt, snapshot.getCreatedAt()); + assertEquals("Clean VM state", snapshot.getDescription()); + assertTrue(snapshot.isOnline()); + } +} diff --git a/src/test/java/tg/cyberlabmanager/model/VMStatusTest.java b/src/test/java/tg/cyberlabmanager/model/VMStatusTest.java new file mode 100644 index 0000000..5f86ffe --- /dev/null +++ b/src/test/java/tg/cyberlabmanager/model/VMStatusTest.java @@ -0,0 +1,18 @@ +package tg.cyberlabmanager.model; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; + +class VMStatusTest { + @Test + void shouldExposeExpectedStableStatuses() { + assertEquals(5, VMStatus.values().length); + assertSame(VMStatus.RUNNING, VMStatus.valueOf("RUNNING")); + assertSame(VMStatus.POWERED_OFF, VMStatus.valueOf("POWERED_OFF")); + assertSame(VMStatus.SAVED, VMStatus.valueOf("SAVED")); + assertSame(VMStatus.PAUSED, VMStatus.valueOf("PAUSED")); + assertSame(VMStatus.UNKNOWN, VMStatus.valueOf("UNKNOWN")); + } +} diff --git a/src/test/java/tg/cyberlabmanager/model/VirtualMachineTest.java b/src/test/java/tg/cyberlabmanager/model/VirtualMachineTest.java new file mode 100644 index 0000000..c986258 --- /dev/null +++ b/src/test/java/tg/cyberlabmanager/model/VirtualMachineTest.java @@ -0,0 +1,61 @@ +package tg.cyberlabmanager.model; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class VirtualMachineTest { + @Test + void shouldUseUnknownStatusByDefault() { + VirtualMachine vm = new VirtualMachine(); + + assertSame(VMStatus.UNKNOWN, vm.getStatus()); + } + + @Test + void shouldStoreVirtualMachineFields() { + VirtualMachine vm = new VirtualMachine(); + + vm.setId(5); + vm.setName("Ubuntu Lab"); + vm.setUuid("vm-uuid-456"); + vm.setDocumentationUrl("https://example.test/doc"); + vm.setStatus(VMStatus.RUNNING); + + assertEquals(5, vm.getId()); + assertEquals("Ubuntu Lab", vm.getName()); + assertEquals("vm-uuid-456", vm.getUuid()); + assertEquals("https://example.test/doc", vm.getDocumentationUrl()); + assertSame(VMStatus.RUNNING, vm.getStatus()); + } + + @Test + void shouldAddSnapshotsAndJournalEntries() { + VirtualMachine vm = new VirtualMachine(); + Snapshot snapshot = new Snapshot(); + JournalEntry journalEntry = new JournalEntry(); + + vm.addSnapshot(snapshot); + vm.addJournalEntry(journalEntry); + + List snapshots = vm.getSnapshots(); + List journalEntries = vm.getJournalEntries(); + + assertEquals(1, snapshots.size()); + assertSame(snapshot, snapshots.get(0)); + assertEquals(1, journalEntries.size()); + assertSame(journalEntry, journalEntries.get(0)); + } + + @Test + void shouldExposeUnmodifiableCollections() { + VirtualMachine vm = new VirtualMachine(); + + assertThrows(UnsupportedOperationException.class, () -> vm.getSnapshots().add(new Snapshot())); + assertThrows(UnsupportedOperationException.class, () -> vm.getJournalEntries().add(new JournalEntry())); + } +}