epitech-tracker — Overview¶
Description¶
epitech-tracker est un agent Python qui tourne comme service systemd sur les laptops Ubuntu 24.04 fournis par EPITECH Côte d'Ivoire. Il observe l'activité de travail de l'étudiant toutes les 5 minutes et envoie un snapshot JSON signé à l'API de la plateforme parentale (Zeno, https://zeno.epitech.africa).
- Repository : EpitechAfrik/epitech-tracker (privé)
- API cible : Zeno, routes
/api/sonde/*— projet et dépôt distincts de celui-ci. Le contrat d'ingestion complet (SONDE_INGESTION.md) et le design de l'enrôlement (SONDE_ENROLMENT.md) vivent dans le dépôt Zeno, pas ici. - Contexte produit : ce signal ("la machine est-elle ouverte et utilisée pour travailler ?") alimente le score d'effort affiché aux parents — voir § ci-dessous.
Le score d'effort¶
D'après le README du projet, le tracker contribue à 15 % d'un score composite :
Les trois autres composantes (tests TEPITECH, activité GitHub, présence campus) sont calculées par d'autres systèmes, hors périmètre de ce dépôt. Le tracker répond uniquement à : "est-ce que l'étudiant a sa machine ouverte et travaille dessus ?".
Le score n'agrège rien tout seul
Le tracker envoie des snapshots bruts. C'est Zeno qui agrège les catégories en un signal d'effort exploitable (travail_seconds, par_categorie) — d'après l'historique du projet (docs/ANALYSE_PROD_2026-09-08.md), l'agrégation côté serveur a longtemps été le maillon manquant : sans elle, 40h de séries et 40h de C produisaient le même signal.
Ce que ça collecte — et ce que ça ne collecte JAMAIS¶
C'est la frontière la plus sensible du projet : la charte étudiant (docs/CHARTE-TEXTE-A-INSERER.md) et la description du paquet Debian (package/DEBIAN/control) l'énoncent explicitement.
Collecté¶
| Donnée | Détail | D'où |
|---|---|---|
| Application active | Nom de process brut (non normalisé), premier plan | observer.py / watcher.py (AT-SPI) |
| Titre de la fenêtre active | Texte complet du titre — transmis au serveur, jamais montré aux parents (voir encart ci-dessous) | idem |
Outil de terminal (outil) |
Process au premier plan d'un terminal (ex: emacs, vim), lu dans /proc/<pid>/stat |
observer.py::foreground_tool |
| Projet git actif | Nom + module + repo, déduits de l'URL remote GitHub d'un dépôt EPITECH ouvert | detector.py |
| Processus de dev | Liste de noms de process (clang, make, git, python…) — pas de contenu | observer.py |
| État de session | Actif/inactif (pas d'entrée clavier/souris depuis 5 min), verrouillage | observer.py |
| Réseau | Connecté oui/non, sur réseau EPITECH oui/non (SSID ou plage IP) | observer.py |
Jamais collecté¶
- Ce que l'étudiant tape (pas de keylogger)
- Le contenu de ses fichiers
- Les URLs visitées
- Des captures d'écran
- L'historique des commandes
Nuance importante : le titre de fenêtre EST transmis au serveur
Le README résume la collecte sans lister explicitement le titre de fenêtre dans son tableau "Collecté", mais le format du snapshot (app_active.titre, fenetre_sessions[].titre) l'inclut bel et bien, et il est envoyé en clair à Zeno. Ce que garantit la charte (docs/CHARTE-TEXTE-A-INSERER.md) et l'architecture (docs/ANALYSE_PROD_2026-09-08.md §10) : le titre brut n'est jamais exposé côté parent — seule la catégorie qu'en déduit le serveur (et le temps agrégé) l'est. Le détail des titres reste accessible côté équipe pédagogique. Si un futur consommateur de l'API Zeno expose un jour le titre brut à un parent, c'est une régression de la promesse faite aux étudiants, pas une décision d'architecture anodine.
Les étudiants signent une charte
Le texte de la charte n'est pas dans ce dépôt (probablement gérée administrativement ailleurs) ; docs/CHARTE-TEXTE-A-INSERER.md documente ce qu'elle doit couvrir pour rester exacte vis-à-vis du code — y compris la clause sur l'analyse du titre des pages web (voir plus bas, § Catégorisation).
Architecture¶
Vue d'ensemble¶
graph TB
subgraph "Laptop étudiant (Ubuntu 24.04)"
Service["epitech-tracker.service<br/>(systemd, démarre au boot)"]
Main["main.py<br/>boucle toutes les 5 min"]
Detector["detector.py<br/>quel projet git est actif ?"]
Observer["observer.py<br/>snapshot activité machine"]
Watcher["watcher.py<br/>WindowWatcher (AT-SPI, event-based)"]
Updater["updater.py<br/>auto-update horaire"]
Sender["sender.py<br/>POST /api/sonde/ingest"]
Queue["queue_manager.py<br/>queue hors-ligne"]
QueueDir["/var/lib/epitech-tracker/queue/"]
end
Zeno["Zeno API<br/>zeno.epitech.africa"]
Service --> Main
Main --> Detector
Main --> Observer
Main --> Watcher
Main --> Updater
Observer --> Sender
Sender -->|en ligne| Zeno
Sender -->|hors ligne| Queue
Queue --> QueueDir
QueueDir -->|sync dès reconnexion| Sender
Updater -->|GET /sonde/version, /sonde/tools.deb| Zeno
Flux d'un cycle (toutes les 5 minutes)¶
detector.detect_active_project()— scanne/proc/*/comm(éditeurs + shells), résoutgit rev-parse+git remote get-url, parse le nom du repo pour en tirer{"nom", "module", "repo"}.observer.collect_snapshot()— assemble le snapshot complet : app active (via leWindowWatcher, avec repli sur une lecture ponctuelle AT-SPI/xdotool si le watcher n'a encore rien), historique des sessions de fenêtres depuis le dernier flush, session (actif/inactif), réseau.sender.send(snapshot)— signe puisPOST /api/sonde/ingest:201/200→ accepté, la queue est vidée si elle contenait des snapshots en attente ;- échec (réseau,
401/403/429/5xx) →queue_manager.push().
La catégorisation n'a PAS lieu ici¶
Le tracker envoie le nom d'application brut ("Google Chrome", pas "chrome"), le titre de la fenêtre et outil. C'est Zeno (server/src/services/sonde/categorize.js, dans le dépôt Zeno) qui normalise et catégorise ; il ignore toute catégorie que la sonde enverrait.
Ce n'est pas toujours resté vrai — historique du choix
D'après git log et docs/ANALYSE_PROD_2026-09-08.md, le tracker a d'abord fait sa propre catégorisation locale (bugs corrigés dans le commit 6473cbb fix(tracker): corriger la catégorisation d'activité), avant qu'un refactor délibéré (c6ea729 refactor(tracker): la sonde ne catégorise plus, elle observe) ne déplace toute la logique côté serveur. Raison documentée : les règles de classement sont versionnées et modifiables sans redéployer ~200 postes, l'historique peut être recalculé, et dupliquer les règles en Python et en JS garantissait qu'elles dérivent en silence (c'est très exactement ce que l'analyse de prod a mis en évidence : Chrome classé "inactif", emacs dans un terminal jamais compté comme dev, etc. — un facteur ~5 de sous-comptage du temps de dev réel). docs/ANALYSE_PROD_2026-09-08.md est donc un document historique de diagnostic, pas une description de l'état actuel du code.
outil reste la seule intelligence locale : le processus au premier plan d'un terminal (lu dans /proc), que le serveur ne peut pas deviner depuis un titre de fenêtre vide.
Authentification machine¶
Enrôlement par clé signée, pas par adresse MAC.
Le README contient une phrase de contexte obsolète
La section « Contexte » du README (§1) dit encore "chaque laptop est identifié de façon permanente par son adresse MAC physique". Le reste du document et le code contredisent cette phrase : l'identification actuelle repose sur deviceId + clé HMAC (identity.json), pas sur la MAC. La configuration NetworkManager qui figeait la MAC physique a même été retirée activement par postinst (voir package/DEBIAN/postinst — "la MAC n'est plus utilisée"). À corriger dans le README source si l'occasion se présente ; ne pas se fier à cette phrase pour comprendre le mécanisme réel d'identification.
Mécanisme réel (identity.py, sender.py) :
- À l'installation,
install.sh(servi par Zeno) appellePOST /api/sonde/enrollavec prénom et nom déclarés par l'étudiant (pas de mail Epitech, pas de matricule — les étudiants n'ont ni l'un ni l'autre) et reçoit{deviceId, key}, écrits dans/etc/epitech-tracker/identity.json(root, 600). - Chaque snapshot est signé :
HMAC-SHA256(key, "deviceId.timestamp")(ou"deviceId.email.timestamp"pour les postes enrôlés par mail avant la 1.0.20, rétrocompatibilité assurée paridentity.sign()). - Un poste non enrôlé ne démarre pas —
postinstrefuse l'installation siidentity.jsonest absent ou vide. - Aucun secret de parc : jusqu'à la version 1.1.0 il existait un PSK (
INGEST_TOKEN, headerX-Sonde-Token) partagé par toutes les machines. Il a été supprimé (56354ed refactor(tracker): plus aucun secret de parc dans la sonde) car un secret partagé par ~200 machines, lisible par tout étudiant root sur son propre laptop, n'ajoutait rien face à une signature HMAC déjà propre à chaque poste.
Mode hors-ligne¶
Snapshot → pas de réseau → queue_manager.push()
└─ fichier JSON dans /var/lib/epitech-tracker/queue/
nommé <timestamp_snapshot>_<deviceId_tronqué>_<suffixe_aléatoire>.json
Cycle suivant, réseau disponible :
sender.flush_queue()
└─ envoi dans l'ordre chronologique, suppression après 201
└─ s'arrête au premier échec (réseau OU 401/403/429/5xx) — réessaie au cycle suivant
Limites : 7 jours de rétention, 50 Mo max (les plus anciens évincés en premier)
Un 401/403 n'efface jamais le snapshot : ces codes sont traités comme transitoires (horloge décalée, ré-enrôlement en attente, clé désynchronisée) — voir sender.py. Seul un 201 déclenche la suppression du fichier de queue.
Format du snapshot¶
Payload envoyé à POST /api/sonde/ingest toutes les 5 minutes. Le contrat complet est maintenu côté Zeno (Zeno/docs/architecture/SONDE_INGESTION.md, hors périmètre de ce dépôt) :
{
"deviceId": "4dccdbbb-e8b4-4b0a-9a16-ec3836db9960",
"timestamp": "2026-09-08T14:35:00Z",
"signature": "…HMAC-SHA256 de deviceId.timestamp…",
"session": { "actif": true, "duree_inactive_minutes": 0 },
"app_active": {
"nom": "gnome-terminal-server",
"titre": "emacs main.c",
"outil": "emacs"
},
"fenetre_sessions": [
{
"app": "gnome-terminal-server",
"titre": "emacs main.c",
"outil": "emacs",
"debut": "2026-09-08T14:30:00Z",
"fin": "2026-09-08T14:35:00Z",
"duree_secondes": 300
}
],
"projet_actif": { "nom": "organized", "module": "CPE-110", "repo": "G-CPE-110-ABJ-1-1-organized-2" },
"processus": ["emacs", "firefox", "docker"],
"reseau": { "connecte": true, "epitech": true }
}
Points notables (src/observer.py, en-tête du module) :
nometappsont bruts, non normalisés — volontaire, voir § catégorisation ci-dessus.- Aucun champ
categorie: envoyée, elle serait ignorée par le serveur. projet_actifvautnullsi aucun dépôt git EPITECH n'est détecté.outilvautnullhors terminal.emailn'apparaît que sur les postes enrôlés par mail (≤ 1.0.20, rétrocompatibilité) ; les postes enrôlés par nom + prénom n'en ont pas.
Packaging (.deb) et déploiement¶
Construction¶
build.sh (racine du dépôt) :
- Vérifie que
package/DEBIAN/controletsrc/config.py(TRACKER_VERSION) annoncent la même version — sinon échec explicite (l'updater comparerait des versions incohérentes). - Copie
src/*.py→package/opt/epitech-tracker/. chmod 755sur les scriptsDEBIAN/(postinst,prerm,postrm).dpkg-deb --build --root-owner-group→epitech-tracker_<version>_all.deb.- Produit en plus
tools.deb(nom fixe),tools.deb.versionettools.deb.sha256— les trois fichiers que Zeno sert respectivement sur/sonde/tools.debet lit via/sonde/version.
dpkg-deb n'existe que sous Linux : sur macOS, un conteneur Debian jetable (debian:stable-slim via Docker/Colima) fait le build.
Distribution — pas via GitHub¶
Les postes se mettent à jour depuis Zeno, pas depuis GitHub :
updater.pyinterrogeGET {API_URL}/sonde/versiontoutes les heures (thread daemon démarré parmain.py), compare àTRACKER_VERSION.- S'il y a une version plus récente : télécharge
GET {API_URL}/sonde/tools.deb, vérifie son sha256 (et sa taille) avant d'installer — un paquet dont le sha256 ne correspond pas est rejeté, jamais installé. - Publier une version = builder, puis déposer
tools.deb+tools.deb.versiondans le conteneur API de Zeno (docker cp ... epitech-api-prod:/app/public/sonde/...), puis taguer ce dépôt (git tag vX.Y.Z) pour que le CI (.github/workflows/release.yml) attache les trois fichiers à une release privée de ce dépôt.
Un ancien parc pouvait pointer vers GitHub — migration déjà exécutée le 2026-09-09
Les postes installés avant la 1.1.0 avaient un updater qui interrogeait api.github.com/repos/YemalinModeste/epitech-tools (dépôt public, personnel, pas celui de l'organisation) et ignorait Zeno. Un workflow_dispatch avec l'option mirror_public permet de migrer ce parc une dernière fois — déjà déclenché avec succès le 2026-09-09 (release v1.1.0 publiée sur ce dépôt, confirmé via gh run list). Le code actuel (1.1.0) n'interroge plus GitHub du tout. Voir runbook.md et continuite-acces-externes.md pour le risque résiduel (poste isolé qui aurait manqué cette fenêtre) et l'implication de continuité (dépôt personnel de Yémalin).
systemd¶
package/lib/systemd/system/epitech-tracker.service :
[Service]
Type=simple
ExecStart=/usr/bin/python3 /opt/epitech-tracker/main.py
Restart=always
RestartSec=30
StandardOutput=append:/var/log/epitech-tracker.log
StandardError=append:/var/log/epitech-tracker.log
User=root
Environment="EPITECH_TRACKER_API_URL=https://zeno.epitech.africa"
EnvironmentFile=-/etc/epitech-tracker/tracker.env
- Tourne en root — nécessaire pour lire
/procet/sysd'autres utilisateurs. Le README liste ceci en TODO haute priorité ("créer un user dédié avec les capabilities minimales"). Restart=always/RestartSec=30: un crash du process redémarre le service, mais avecStartLimitIntervalSec=60/StartLimitBurst=5— au-delà de 5 crashs en 60s, systemd cesse de redémarrer (voir troubleshooting.md).EnvironmentFile=-/etc/epitech-tracker/tracker.env(le-le rend optionnel) porteEPITECH_TRACKER_API_URLposé à l'install parinstall.sh.
Immuabilité anti-tamper¶
postinst pose chattr +i sur /usr/lib/systemd/system/epitech-tracker.service et -R +i sur /opt/epitech-tracker/ — un étudiant root sur sa propre machine ne peut pas trivialement désactiver ou modifier le service. updater.py::_lift_immutability() retire cet attribut avant chaque mise à jour automatique (chattr -i), et prerm fait de même avant un dpkg manuel. identity.json n'est jamais rendu immuable : il doit rester ré-enrôlable.
Ce qui n'est PAS dans ce dépôt¶
- L'API Zeno elle-même (
/api/sonde/enroll,/api/sonde/ingest,/sonde/version,/sonde/tools.deb, agrégation, catégorisation, écran de validation pédago). C'est un projet et un dépôt distincts — ne pas en documenter le contrat ici sans l'avoir vérifié dans ce dépôt. - La charte étudiant signée — seul le texte à y intégrer est présent (
docs/CHARTE-TEXTE-A-INSERER.md). - Le tableau de bord de suivi (
sonde-dashboard.html,tools/build-sonde-dashboard.mjs) : outil de dev local pour visualiser un export de snapshots, pas un service déployé.