Files
rpa_vision_v3/tests/unit/test_system_dialog_guard.py
Dom aee64f54b1 feat(security): détection dialogues système Windows + fail-closed
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>
2026-04-14 16:48:00 +02:00

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"])