Nouveau module system_dialog_guard.py : - Détection UAC, CredUI, SmartScreen, Defender, Driver install - Multi-signal (ClassName UIA, process, title FR/EN, parent_path) - Faux positifs validés (OSIRIS, OBSIUS, MEDSPHERE, Chrome, Excel) Intégration dans executor.py et policy.py : - 6 points de décision (avant click/type/key_combo, VLM, policy) - Pause supervisée au lieu de clic aveugle - Fail-closed en cas d'exception (P0-D audit) - Notification systray + remontée serveur Fix mock test policy engine pour compat _system_dialog_pause=None. 39 + 5 tests unitaires. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
392 lines
14 KiB
Python
392 lines
14 KiB
Python
# tests/unit/test_system_dialog_guard.py
|
|
"""
|
|
Tests du garde-fou sécurité : détection des dialogues système Windows critiques.
|
|
|
|
Objectif : garantir que Léa REFUSE de cliquer automatiquement sur un
|
|
UAC / CredUI / SmartScreen (vecteur d'attaque ransomware).
|
|
|
|
Philosophie :
|
|
- Faux positif tolérable (pause pour rien).
|
|
- Faux négatif catastrophique (clic UAC).
|
|
- Les tests privilégient la sécurité : tout dialogue suspect DOIT matcher.
|
|
|
|
Cf. agent_v0/agent_v1/core/system_dialog_guard.py
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from agent_v0.agent_v1.core.system_dialog_guard import (
|
|
SystemDialogCategory,
|
|
SystemDialogDetection,
|
|
is_system_dialog,
|
|
)
|
|
|
|
|
|
# =============================================================================
|
|
# UAC (Contrôle de compte d'utilisateur) — le danger le plus grave
|
|
# =============================================================================
|
|
|
|
|
|
class TestUACDetection:
|
|
"""Un UAC qui n'est PAS détecté = vecteur d'attaque ransomware."""
|
|
|
|
def test_uac_via_class_name_exact(self):
|
|
"""ClassName UIA du Consent.exe."""
|
|
d = is_system_dialog(
|
|
uia_snapshot={"class_name": "$$$Secure UAP Dummy Window Class$$$"}
|
|
)
|
|
assert d.is_system_dialog
|
|
assert d.category == SystemDialogCategory.UAC
|
|
assert d.matched_signal == "class_name"
|
|
|
|
def test_uac_via_class_name_case_variation(self):
|
|
"""Robustesse à la casse (UIA peut varier)."""
|
|
d = is_system_dialog(
|
|
uia_snapshot={"class_name": "$$$SECURE UAP DUMMY WINDOW CLASS$$$"}
|
|
)
|
|
assert d.is_system_dialog
|
|
assert d.category == SystemDialogCategory.UAC
|
|
|
|
def test_uac_via_class_name_fuzzy_secure_uap(self):
|
|
"""Détection souple si Microsoft ajoute un suffixe."""
|
|
d = is_system_dialog(
|
|
uia_snapshot={"class_name": "Secure UAP Dummy"}
|
|
)
|
|
assert d.is_system_dialog
|
|
assert d.category == SystemDialogCategory.UAC
|
|
|
|
def test_uac_via_process_consent_exe(self):
|
|
"""Le process consent.exe signe un UAC, peu importe la classe."""
|
|
d = is_system_dialog(uia_snapshot={"process_name": "consent.exe"})
|
|
assert d.is_system_dialog
|
|
assert d.category == SystemDialogCategory.UAC
|
|
|
|
def test_uac_via_process_consent_exe_with_path(self):
|
|
"""Chemin complet doit être normalisé."""
|
|
d = is_system_dialog(
|
|
uia_snapshot={"process_name": r"C:\Windows\System32\consent.exe"}
|
|
)
|
|
assert d.is_system_dialog
|
|
assert d.category == SystemDialogCategory.UAC
|
|
|
|
def test_uac_via_app_name_in_window_info(self):
|
|
"""psutil.name() (app_name côté agent) doit être reconnu."""
|
|
d = is_system_dialog(
|
|
window_info={"title": "Administrateur", "app_name": "consent.exe"}
|
|
)
|
|
assert d.is_system_dialog
|
|
assert d.category == SystemDialogCategory.UAC
|
|
|
|
def test_uac_via_title_fr(self):
|
|
"""Titre français officiel."""
|
|
d = is_system_dialog(
|
|
window_info={"title": "Contrôle de compte d'utilisateur"}
|
|
)
|
|
assert d.is_system_dialog
|
|
assert d.category == SystemDialogCategory.UAC
|
|
|
|
def test_uac_via_title_en(self):
|
|
"""Titre anglais."""
|
|
d = is_system_dialog(window_info={"title": "User Account Control"})
|
|
assert d.is_system_dialog
|
|
assert d.category == SystemDialogCategory.UAC
|
|
|
|
def test_uac_via_title_question_fr(self):
|
|
"""Phrase caractéristique du prompt UAC."""
|
|
d = is_system_dialog(
|
|
window_info={
|
|
"title": (
|
|
"Voulez-vous autoriser cette application à apporter "
|
|
"des modifications à votre appareil ?"
|
|
)
|
|
}
|
|
)
|
|
assert d.is_system_dialog
|
|
assert d.category == SystemDialogCategory.UAC
|
|
|
|
def test_uac_via_title_question_en(self):
|
|
d = is_system_dialog(
|
|
window_info={
|
|
"title": "Do you want to allow this app to make changes to your device?"
|
|
}
|
|
)
|
|
assert d.is_system_dialog
|
|
assert d.category == SystemDialogCategory.UAC
|
|
|
|
def test_uac_via_parent_path_button_focused(self):
|
|
"""Cas critique : le focus est sur le bouton 'Oui' de l'UAC.
|
|
|
|
Le ClassName du Button sera "Button", mais le parent est bien
|
|
le Consent.exe. La détection DOIT matcher sur parent_path.
|
|
"""
|
|
d = is_system_dialog(
|
|
uia_snapshot={
|
|
"class_name": "Button",
|
|
"name": "Oui",
|
|
"control_type": "Button",
|
|
"parent_path": [
|
|
{"class_name": "$$$Secure UAP Dummy Window Class$$$", "name": ""},
|
|
],
|
|
}
|
|
)
|
|
assert d.is_system_dialog
|
|
assert d.category == SystemDialogCategory.UAC
|
|
assert "parent" in d.matched_signal.lower() or d.matched_signal == "parent_class_name"
|
|
|
|
|
|
# =============================================================================
|
|
# CredUI (prompt mot de passe Windows)
|
|
# =============================================================================
|
|
|
|
|
|
class TestCredUIDetection:
|
|
"""Les prompts de mot de passe ne doivent JAMAIS recevoir de frappe auto."""
|
|
|
|
def test_credui_via_class_name(self):
|
|
d = is_system_dialog(
|
|
uia_snapshot={"class_name": "Credential Dialog Xaml Host"}
|
|
)
|
|
assert d.is_system_dialog
|
|
assert d.category == SystemDialogCategory.CREDUI
|
|
|
|
def test_credui_variant_no_space(self):
|
|
"""Variante sans espaces."""
|
|
d = is_system_dialog(
|
|
uia_snapshot={"class_name": "CredentialDialogXamlHost"}
|
|
)
|
|
assert d.is_system_dialog
|
|
assert d.category == SystemDialogCategory.CREDUI
|
|
|
|
def test_credui_via_process_credentialuibroker(self):
|
|
d = is_system_dialog(
|
|
uia_snapshot={"process_name": "CredentialUIBroker.exe"}
|
|
)
|
|
assert d.is_system_dialog
|
|
assert d.category == SystemDialogCategory.CREDUI
|
|
|
|
def test_credui_via_process_credui(self):
|
|
d = is_system_dialog(uia_snapshot={"process_name": "credui.exe"})
|
|
assert d.is_system_dialog
|
|
assert d.category == SystemDialogCategory.CREDUI
|
|
|
|
def test_credui_via_title_fr(self):
|
|
d = is_system_dialog(window_info={"title": "Sécurité Windows"})
|
|
assert d.is_system_dialog
|
|
assert d.category == SystemDialogCategory.CREDUI
|
|
|
|
def test_credui_via_title_en(self):
|
|
d = is_system_dialog(window_info={"title": "Windows Security"})
|
|
assert d.is_system_dialog
|
|
assert d.category == SystemDialogCategory.CREDUI
|
|
|
|
def test_credui_via_title_enter_creds(self):
|
|
d = is_system_dialog(
|
|
window_info={"title": "Connectez-vous à votre compte"}
|
|
)
|
|
assert d.is_system_dialog
|
|
assert d.category == SystemDialogCategory.CREDUI
|
|
|
|
|
|
# =============================================================================
|
|
# SmartScreen
|
|
# =============================================================================
|
|
|
|
|
|
class TestSmartScreenDetection:
|
|
|
|
def test_smartscreen_via_process(self):
|
|
d = is_system_dialog(uia_snapshot={"process_name": "smartscreen.exe"})
|
|
assert d.is_system_dialog
|
|
assert d.category == SystemDialogCategory.SMARTSCREEN
|
|
|
|
def test_smartscreen_via_title_fr(self):
|
|
d = is_system_dialog(
|
|
window_info={"title": "Windows a protégé votre ordinateur"}
|
|
)
|
|
assert d.is_system_dialog
|
|
assert d.category == SystemDialogCategory.SMARTSCREEN
|
|
|
|
def test_smartscreen_via_title_en(self):
|
|
d = is_system_dialog(
|
|
window_info={"title": "Windows protected your PC"}
|
|
)
|
|
assert d.is_system_dialog
|
|
assert d.category == SystemDialogCategory.SMARTSCREEN
|
|
|
|
def test_smartscreen_unknown_publisher(self):
|
|
d = is_system_dialog(
|
|
window_info={"title": "Éditeur inconnu — Voulez-vous continuer ?"}
|
|
)
|
|
assert d.is_system_dialog
|
|
assert d.category == SystemDialogCategory.SMARTSCREEN
|
|
|
|
|
|
# =============================================================================
|
|
# Autres dialogues système
|
|
# =============================================================================
|
|
|
|
|
|
class TestOtherSystemDialogs:
|
|
|
|
def test_defender_via_process(self):
|
|
d = is_system_dialog(uia_snapshot={"process_name": "MsMpEng.exe"})
|
|
assert d.is_system_dialog
|
|
assert d.category == SystemDialogCategory.DEFENDER
|
|
|
|
def test_defender_threat_detected(self):
|
|
d = is_system_dialog(
|
|
window_info={"title": "Menace détectée — Windows Defender"}
|
|
)
|
|
assert d.is_system_dialog
|
|
# Le regex "windows defender" gagne (listé en premier dans les patterns)
|
|
assert d.category == SystemDialogCategory.DEFENDER
|
|
|
|
def test_driver_install(self):
|
|
d = is_system_dialog(
|
|
window_info={"title": "Installer ce pilote logiciel ?"}
|
|
)
|
|
assert d.is_system_dialog
|
|
assert d.category == SystemDialogCategory.DRIVER
|
|
|
|
|
|
# =============================================================================
|
|
# FAUX POSITIFS À ÉVITER — les apps métier doivent passer
|
|
# =============================================================================
|
|
|
|
|
|
class TestBusinessAppsPassThrough:
|
|
"""Vérifier que les apps métier (OSIRIS, OBSIUS, MEDSPHERE, etc.)
|
|
ne sont jamais confondues avec des dialogues système.
|
|
|
|
Un faux positif ici = workflow métier cassé.
|
|
"""
|
|
|
|
def test_osiris_main_window(self):
|
|
d = is_system_dialog(
|
|
window_info={"title": "OSIRIS - Patient Dupont Jean", "app_name": "osiris.exe"}
|
|
)
|
|
assert not d.is_system_dialog
|
|
|
|
def test_osiris_confirmation_dialog(self):
|
|
"""Un dialogue #32770 OSIRIS métier (confirmation sauvegarde) DOIT passer."""
|
|
d = is_system_dialog(
|
|
uia_snapshot={
|
|
"class_name": "#32770",
|
|
"name": "Confirmation",
|
|
"process_name": "osiris.exe",
|
|
},
|
|
window_info={"title": "Confirmation", "app_name": "osiris.exe"},
|
|
)
|
|
assert not d.is_system_dialog
|
|
|
|
def test_obsius_main_window(self):
|
|
d = is_system_dialog(
|
|
window_info={"title": "OBSIUS - Consultation", "app_name": "obsius.exe"}
|
|
)
|
|
assert not d.is_system_dialog
|
|
|
|
def test_medsphere(self):
|
|
d = is_system_dialog(
|
|
window_info={"title": "MEDSPHERE v4.2", "app_name": "medsphere.exe"}
|
|
)
|
|
assert not d.is_system_dialog
|
|
|
|
def test_chrome_with_security_word_in_page(self):
|
|
"""Chrome avec 'sécurité' dans le titre de page = OK si app_name=chrome.exe.
|
|
|
|
NOTE : on accepte ici un faux positif théorique car 'Sécurité Windows'
|
|
est matché par le regex titre. Un navigateur affichant cette exacte
|
|
phrase en titre est possible. Compte tenu de l'asymétrie (clic UAC =
|
|
catastrophe), on ACCEPTE cette pause supervisée de fait.
|
|
"""
|
|
# Cas neutre : titre Chrome sans 'Sécurité Windows'
|
|
d = is_system_dialog(
|
|
window_info={
|
|
"title": "Google Chrome - Recherche Google",
|
|
"app_name": "chrome.exe",
|
|
}
|
|
)
|
|
assert not d.is_system_dialog
|
|
|
|
def test_excel_file(self):
|
|
d = is_system_dialog(
|
|
window_info={"title": "Classeur1 - Excel", "app_name": "EXCEL.EXE"}
|
|
)
|
|
assert not d.is_system_dialog
|
|
|
|
def test_notepad(self):
|
|
d = is_system_dialog(
|
|
window_info={"title": "Sans titre - Bloc-notes", "app_name": "notepad.exe"}
|
|
)
|
|
assert not d.is_system_dialog
|
|
|
|
|
|
# =============================================================================
|
|
# Cas limites — robustesse
|
|
# =============================================================================
|
|
|
|
|
|
class TestEdgeCases:
|
|
|
|
def test_no_input_returns_false(self):
|
|
"""Aucune info disponible = pas de blocage (fail-open)."""
|
|
d = is_system_dialog()
|
|
assert not d.is_system_dialog
|
|
|
|
def test_empty_strings(self):
|
|
d = is_system_dialog(
|
|
uia_snapshot={"class_name": "", "process_name": "", "name": ""},
|
|
window_info={"title": "", "app_name": ""},
|
|
)
|
|
assert not d.is_system_dialog
|
|
|
|
def test_none_values(self):
|
|
"""Les champs à None ne doivent pas planter."""
|
|
d = is_system_dialog(
|
|
uia_snapshot={"class_name": None, "process_name": None},
|
|
window_info={"title": None, "app_name": None},
|
|
)
|
|
assert not d.is_system_dialog
|
|
|
|
def test_detection_to_dict_serializable(self):
|
|
d = is_system_dialog(
|
|
window_info={"title": "User Account Control"}
|
|
)
|
|
data = d.to_dict()
|
|
assert data["is_system_dialog"] is True
|
|
assert data["category"] == SystemDialogCategory.UAC
|
|
assert data["reason"]
|
|
|
|
def test_unicode_title_fr_accents(self):
|
|
"""Les accents ne cassent pas la détection."""
|
|
d = is_system_dialog(
|
|
window_info={"title": "Contrôle de compte d'utilisateur"}
|
|
)
|
|
assert d.is_system_dialog
|
|
|
|
def test_whitespace_title(self):
|
|
d = is_system_dialog(
|
|
window_info={"title": " Windows Security "}
|
|
)
|
|
assert d.is_system_dialog
|
|
|
|
|
|
# =============================================================================
|
|
# Intégration avec detect_current_system_dialog
|
|
# =============================================================================
|
|
|
|
|
|
def test_detect_current_system_dialog_no_exception_linux():
|
|
"""Sur Linux, UIA indispo mais l'appel ne doit jamais planter."""
|
|
from agent_v0.agent_v1.core.system_dialog_guard import detect_current_system_dialog
|
|
detection = detect_current_system_dialog()
|
|
# On ne valide pas le résultat (dépend de la fenêtre active) —
|
|
# juste qu'il n'y a pas d'exception.
|
|
assert isinstance(detection, SystemDialogDetection)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|