Files
claude-regelwerk/.vault-tools/trading-plattform-pruefung/browserlauf_zugang.py
T
Pascal MichelandClaude Opus 5 c115a13190 Automatischer Push: 11 Datei(en) geändert
- .vault-tools/trading-plattform-pruefung/LIESMICH.md
- .vault-tools/trading-plattform-pruefung/abendkontrolle.py
- .vault-tools/trading-plattform-pruefung/abendkontrolle_angriffe.py
- .vault-tools/trading-plattform-pruefung/abendkontrolle_mutanten.py
- .vault-tools/trading-plattform-pruefung/abendkontrolle_wsl.py
- .vault-tools/trading-plattform-pruefung/browserlauf.py
- .vault-tools/trading-plattform-pruefung/browserlauf_zugang.py
- .vault-tools/trading-plattform-pruefung/ha_totmann.py
- .vault-tools/trading-plattform-pruefung/ha_totmann_wsl.py
- .vault-tools/trading-plattform-pruefung/scan_push.sh
- AI/Regelwerk/codeworkflow.md

Erzeugt vom PostToolUse-Hook .vault-tools/auto-push/auto_push.py.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-26 04:00:02 +02:00

191 lines
9.5 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: das Secure-Merkmal und HSTS (dafuer angriffe.py mit einer https-Adresse), ob das
Zertifikat taugt (--selbstsigniert schaltet die Zertifikatspruefung des Browsers ab), 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.
Unter https belegt der Lauf, dass Anmeldung, Strom (wss) und alle Ansichten ueber TLS tragen: Ein
Browser schickt ein Secure-Cookie nur ueber https zurueck, und ohne Cookie gaebe es keinen Strom.
Aufruf: python -X utf8 browserlauf_zugang.py <basis-url> <zugang.json> <ausgabeordner> [--sabotage] [--selbstsigniert]
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:]
selbstsigniert = "--selbstsigniert" 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}, ignore_https_errors=selbstsigniert)
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", exact=True).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", exact=True).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)