- .vault-tools/trading-plattform-pruefung/LIESMICH.md - .vault-tools/trading-plattform-pruefung/angriffe.py - .vault-tools/trading-plattform-pruefung/browserlauf_zugang.py - .vault-tools/trading-plattform-pruefung/handabruf_kerzen.py - .vault-tools/trading-plattform-pruefung/lies_paper_protokoll.py - .vault-tools/trading-plattform-pruefung/pruef_beobachtung_echt.py - .vault-tools/trading-plattform-pruefung/start_probe_backend.py - .vault-tools/trading-plattform-pruefung/start_probe_zugang.py Erzeugt vom PostToolUse-Hook .vault-tools/auto-push/auto_push.py. Co-Authored-By: Claude Opus 5 <[email protected]>
187 lines
9.1 KiB
Python
187 lines
9.1 KiB
Python
"""Browser-Lauf zur Anmeldung (Teil 10) mit Playwright (Windows-Python), gegen das Probe-Backend mit Zugang.
|
|
|
|
Koordinator-Pruefskript, nicht fuers Repo; ein Fangnetz, kein Nachweis.
|
|
|
|
Fragen (Herkunft: Projektvorgabe, Spec Teil 10, Teil B):
|
|
- Zeigt die Oberflaeche ohne Sitzung das Anmeldeformular und NICHT die App?
|
|
- Fuehrt eine falsche Anmeldung zu einer Fehlerzeile (api.ts: "Benutzer, Passwort oder Code stimmt nicht.") und nicht in die App?
|
|
- Fuehrt die richtige Anmeldung (Passwort plus frischer TOTP-Code) in die App, mit Benutzername und
|
|
Knopf "Abmelden" in der Kopfzeile, laufendem Strom ("Backend verbunden") und der Karte "Paper-Betrieb"?
|
|
- Traegt die CSP die gebaute Oberflaeche: keine Konsolenmeldung, die "Content Security Policy" nennt,
|
|
in keiner Ansicht (auch "Markt" mit dem Chart)?
|
|
- Fuehrt "Abmelden" zurueck zum Formular, und bleibt es nach dem Neuladen dort?
|
|
Was es nicht sieht: TLS und das Secure-Merkmal, Safari und Firefox (nur Chromium), ob es gut aussieht
|
|
(dafuer die Screenshots, die der Koordinator selbst ansieht).
|
|
Regressionsnachweis: --sabotage erwartet nach der FALSCHEN Anmeldung die App und muss rot enden.
|
|
|
|
Aufruf: python -X utf8 browserlauf_zugang.py <basis-url> <zugang.json> <ausgabeordner> [--sabotage]
|
|
Exit 0 ohne Befund, 2 mit Befund, 3 wenn eine Voraussetzung fehlt.
|
|
"""
|
|
|
|
import base64
|
|
import hashlib
|
|
import hmac
|
|
import json
|
|
import struct
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
from playwright.sync_api import TimeoutError as PlaywrightTimeout
|
|
from playwright.sync_api import sync_playwright
|
|
|
|
|
|
def totp(geheimnis: str, zeit_s: float) -> str:
|
|
schluessel = base64.b32decode(geheimnis.upper() + "=" * (-len(geheimnis) % 8))
|
|
mac = hmac.new(schluessel, struct.pack(">Q", int(zeit_s // 30)), hashlib.sha1).digest()
|
|
versatz = mac[-1] & 0x0F
|
|
zahl = struct.unpack(">I", mac[versatz : versatz + 4])[0] & 0x7FFFFFFF
|
|
return f"{zahl % 1_000_000:06d}"
|
|
|
|
|
|
def main() -> int:
|
|
if len(sys.argv) < 4:
|
|
print(__doc__)
|
|
return 3
|
|
basis = sys.argv[1].rstrip("/")
|
|
zugang_datei = Path(sys.argv[2])
|
|
ziel = Path(sys.argv[3])
|
|
sabotage = "--sabotage" in sys.argv[4:]
|
|
if not zugang_datei.is_file():
|
|
print(f"VORAUSSETZUNG FEHLT: {zugang_datei}")
|
|
return 3
|
|
zugang = json.loads(zugang_datei.read_text(encoding="utf-8"))
|
|
ziel.mkdir(parents=True, exist_ok=True)
|
|
|
|
befunde: list[str] = []
|
|
konsole: list[str] = []
|
|
antworten: list[str] = [] # jede Antwort ab 400 mit Phase, Status und Pfad
|
|
phase = ["start"]
|
|
|
|
def pruefe(name: str, bedingung: bool, einzelheit: str = "") -> None:
|
|
print((" ok " if bedingung else " ROT ") + name + ("" if bedingung else f": {einzelheit}"))
|
|
if not bedingung:
|
|
befunde.append(f"{name}: {einzelheit}")
|
|
|
|
with sync_playwright() as spiel:
|
|
browser = spiel.chromium.launch()
|
|
seite = browser.new_page(viewport={"width": 1440, "height": 900})
|
|
seite.on("console", lambda m: konsole.append(f"{m.type}: {m.text}") if m.type in ("error", "warning") else None)
|
|
seite.on("pageerror", lambda f: konsole.append(f"pageerror: {f}"))
|
|
seite.on(
|
|
"response",
|
|
lambda a: antworten.append(f"{phase[0]} {a.status} {a.request.method} {a.url.replace(basis, '')}")
|
|
if a.status >= 400
|
|
else None,
|
|
)
|
|
|
|
def formular_da() -> bool:
|
|
try:
|
|
seite.get_by_label("Passwort").wait_for(timeout=8000)
|
|
return True
|
|
except PlaywrightTimeout:
|
|
return False
|
|
|
|
def app_da() -> bool:
|
|
try:
|
|
seite.get_by_role("button", name="Abmelden").wait_for(timeout=8000)
|
|
return True
|
|
except PlaywrightTimeout:
|
|
return False
|
|
|
|
print("== ohne Sitzung")
|
|
phase[0] = "ohne Sitzung"
|
|
seite.goto(basis + "/")
|
|
pruefe("Anmeldeformular sichtbar", formular_da(), "kein Feld 'Passwort'")
|
|
pruefe("App nicht sichtbar", seite.get_by_role("button", name="Abmelden").count() == 0, "Knopf 'Abmelden' ist da")
|
|
seite.screenshot(path=str(ziel / "01-anmeldung.png"))
|
|
|
|
print("== falsche Anmeldung")
|
|
phase[0] = "falsche Anmeldung"
|
|
seite.get_by_label("Benutzer").fill(zugang["benutzer"])
|
|
seite.get_by_label("Passwort").fill("ganz-falsches-passwort")
|
|
seite.get_by_label("Code").fill("000000")
|
|
seite.get_by_role("button", name="Anmelden").click()
|
|
seite.wait_for_timeout(1500)
|
|
if sabotage:
|
|
pruefe("SABOTAGE: nach der falschen Anmeldung ist die App sichtbar", app_da(), "App nicht sichtbar (so soll es sein)")
|
|
else:
|
|
pruefe("weiter das Formular", seite.get_by_label("Passwort").count() == 1, "Formular weg")
|
|
pruefe("Fehlerzeile 'stimmt nicht'", seite.get_by_text("stimmt nicht").count() >= 1, "kein Fehlertext")
|
|
seite.screenshot(path=str(ziel / "02-abgelehnt.png"))
|
|
|
|
print("== richtige Anmeldung")
|
|
phase[0] = "richtige Anmeldung"
|
|
# Ein frischer Zeitschritt, falls der Code dieses Schritts gerade verbraucht wurde.
|
|
rest = 30 - (time.time() % 30)
|
|
if rest < 5:
|
|
time.sleep(rest + 1)
|
|
seite.get_by_label("Passwort").fill(zugang["passwort"])
|
|
seite.get_by_label("Code").fill(totp(zugang["geheimnis"], time.time()))
|
|
seite.get_by_role("button", name="Anmelden").click()
|
|
pruefe("App sichtbar (Knopf 'Abmelden')", app_da(), "kein Knopf 'Abmelden'")
|
|
pruefe("Benutzername in der Kopfzeile", seite.get_by_text(zugang["benutzer"], exact=True).count() >= 1, "Name fehlt")
|
|
try:
|
|
seite.get_by_text("Backend verbunden").wait_for(timeout=15000)
|
|
verbunden = True
|
|
except PlaywrightTimeout:
|
|
verbunden = False
|
|
pruefe("Strom laeuft ('Backend verbunden')", verbunden, "Statuspunkt zeigt es nicht")
|
|
pruefe("Karte 'Paper-Betrieb' da", seite.get_by_text("Paper-Betrieb").count() >= 1, "Karte fehlt")
|
|
seite.wait_for_timeout(1500)
|
|
seite.screenshot(path=str(ziel / "03-uebersicht.png"), full_page=True)
|
|
|
|
print("== Ansichten unter der CSP")
|
|
phase[0] = "Ansichten unter der CSP"
|
|
for name in ("Agenten", "Laeufe", "Markt", "Bestand"):
|
|
knopf = seite.get_by_role("button", name=name.replace("ae", "ä") if name == "Laeufe" else name)
|
|
if knopf.count() == 0:
|
|
knopf = seite.get_by_role("link", name=name.replace("ae", "ä") if name == "Laeufe" else name)
|
|
if knopf.count() == 0:
|
|
pruefe(f"Ansicht {name} erreichbar", False, "kein Knopf und kein Link mit diesem Namen")
|
|
continue
|
|
knopf.first.click()
|
|
seite.wait_for_timeout(2500)
|
|
seite.screenshot(path=str(ziel / f"04-{name.lower()}.png"))
|
|
csp_meldungen = [m for m in konsole if "Content Security Policy" in m or "content security policy" in m.lower()]
|
|
pruefe("keine CSP-Meldung in der Konsole", not csp_meldungen, " | ".join(csp_meldungen[:3]))
|
|
# Chromium schreibt jede Antwort ab 400 als Konsolenfehler. Die 401 der absichtlich falschen
|
|
# Anmeldung ist erwartet; jede andere Antwort ab 400 wird mit Phase und Pfad zum Befund.
|
|
unerwartet = [a for a in antworten if not (" 401 POST /api/anmeldung" in a and a.startswith("falsche Anmeldung"))]
|
|
pruefe("keine unerwartete Antwort ab 400", not unerwartet, " | ".join(unerwartet[:5]))
|
|
andere = [m for m in konsole if m not in csp_meldungen and "Failed to load resource" not in m]
|
|
pruefe("keine andere Fehlermeldung in der Konsole", not andere, " | ".join(andere[:3]))
|
|
|
|
print("== Abmelden")
|
|
phase[0] = "Abmelden"
|
|
seite.get_by_role("button", name="Abmelden").click()
|
|
pruefe("zurueck zum Formular", formular_da(), "Formular erscheint nicht")
|
|
seite.reload()
|
|
pruefe("nach dem Neuladen weiter das Formular", formular_da(), "Formular erscheint nicht")
|
|
seite.screenshot(path=str(ziel / "05-abgemeldet.png"))
|
|
|
|
print("== schmale Breite")
|
|
phase[0] = "schmale Breite"
|
|
seite.set_viewport_size({"width": 375, "height": 800})
|
|
seite.reload()
|
|
pruefe("Formular bei 375 px", formular_da(), "Formular erscheint nicht")
|
|
masse = seite.evaluate("() => ({scroll: document.documentElement.scrollWidth, sicht: document.documentElement.clientWidth})")
|
|
pruefe("kein waagerechter Scroll bei 375 px", masse["scroll"] <= masse["sicht"], str(masse))
|
|
seite.screenshot(path=str(ziel / "06-anmeldung-375.png"))
|
|
browser.close()
|
|
|
|
(ziel / "konsole.json").write_text(json.dumps(konsole, ensure_ascii=False, indent=1), encoding="utf-8")
|
|
(ziel / "antworten-ab-400.json").write_text(json.dumps(antworten, ensure_ascii=False, indent=1), encoding="utf-8")
|
|
print(f"\n{len(befunde)} Befund(e); Screenshots unter {ziel}")
|
|
for befund in befunde:
|
|
print("BEFUND:", befund)
|
|
return 2 if befunde else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
sys.exit(main())
|
|
except Exception as ausnahme: # noqa: BLE001 - ein Absturz des Pruefskripts ist selbst ein Befund
|
|
print(f"BEFUND: das Pruefskript ist abgebrochen: {ausnahme!r}")
|
|
sys.exit(2)
|