Pruefwerkzeuge Trading Plattform: Abendkontrolle vom Docker-Host (Paket DH), abgedichtete Angriffstreiber; plankritik.js auf LF

- abendkontrolle.py: --quelle und --quelle-ibkr (Vorgabe wsl wie bisher, dockerhost liest das Ergebnis des Hosts ueber SSH), fahre_dockerhost
- abendkontrolle_wsl.py: --ibkr-python fuer ibkr_pruefen.py im Container
- abendkontrolle_einrichten.ps1: -Quelle und -QuelleIbkr, 23:15 sobald ein Teil vom Host kommt
- neue Angriffstreiber abendkontrolle_dockerhost_angriffe.py und abendkontrolle_ibkr_python_angriffe.py
- Abdichtung gegen echte WSL- und SSH-Wege in den Treibern, auch in abendkontrolle_angriffe.py und rueckweg_aapl_angriffe.py (nach dem ntfy-Vorfall eines Mutanten am 2026-09-26)
- LIESMICH: Abschnitt Paket DH
- .claude/workflows/plankritik.js: LF statt CRLF (das CR war die Ursache von "script contains control characters"), meta gekuerzt, Einzelheiten im Kommentar

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Pascal MichelandClaude Opus 5.5 committed 2026-09-26 14:53:53 +02:00
1 parent c115a13190
commit 59087fb235
9 files changed
+588 -16

No files matched your search

@@ -0,0 +1,291 @@
"""Angriffsfaelle fuer die Quellen wsl und dockerhost von abendkontrolle.py (Paket DH), unter Windows, ohne Netz.
Koordinator-Werkzeug, nicht fuers Repo. Ersetzt ssh.exe durch eine .cmd-Attrappe, die ein kleines
Python-Skript startet; der Vault ist ein Wegwerf-Ordner. Die Attrappe schreibt jeden Aufruf mit
und antwortet je nach ANGRIFF_ART mit einem Ergebnis des angefragten Teils (JJJJ-MM-TT oder
JJJJ-MM-TT-ibkr), mit Exit 3 (noch nichts da), mit Muell oder mit einem Ergebnis des falschen
Teils. Der Treiber faengt je Fall auch SystemExit: Gegen ein abendkontrolle.py ohne --quelle
bricht argparse damit ab, und das soll ein Fall sein, nicht das Ende des Laufs.
Abgedichtet (Plankritik DH, 2026-09-26): WSL zeigt in jedem Lauf ins Leere, und fahre_wsl ist
ohne eigene Attrappe des Falls eine Attrappe, die ablehnt; ein Aufruf des echten Wegs ist eine
Abweichung, kein Lauf. Zwei Faelle belegen beide Sperren.
Aufruf: python abendkontrolle_dockerhost_angriffe.py
Exit 0 ohne Abweichung, 2 mit Abweichung.
"""
import contextlib
import importlib.util
import io
import json
import os
import sys
import tempfile
from datetime import datetime
from pathlib import Path
HIER = Path(__file__).resolve().parent
ATTRAPPE_PY = r'''
import json
import os
import sys
with open(os.environ["ANGRIFF_PROTOKOLL"], "a", encoding="utf-8") as datei:
datei.write(json.dumps(sys.argv[1:]) + "\n")
art = os.environ.get("ANGRIFF_ART", "gut")
if art == "fehlt":
sys.stderr.write("abendkontrolle-lesen: fuer diesen Tag liegt noch kein Ergebnis\n")
sys.exit(3)
if art == "muell":
print("kein json")
sys.exit(0)
ziel = sys.argv[-1]
ibkr = ziel.endswith("-ibkr")
tag = ziel[:-5] if ibkr else ziel
teil = "ibkr" if ibkr and art != "falscher_teil" else "alpaca"
print(json.dumps({"tag": tag, "teil": teil, "befunde": [], "hinweise": [], "fakten": {"handelstag": True}, "exit": 0}))
'''
FAELLE = []
def fall(funktion):
FAELLE.append(funktion)
return funktion
def lade():
spec = importlib.util.spec_from_file_location("abendkontrolle_unter_angriff", HIER / "abendkontrolle.py")
modul = importlib.util.module_from_spec(spec)
spec.loader.exec_module(modul)
return modul
def lauf(art, *argumente, modul=None):
"""Ein Lauf von main() in einem Wegwerf-Vault; liefert Exit, Notiz, Ergebnisdateien und ssh-Aufrufe."""
with tempfile.TemporaryDirectory() as ordner:
wurzel = Path(ordner)
vault = wurzel / "vault"
(vault / "Inbox").mkdir(parents=True)
protokoll = wurzel / "ssh.jsonl"
attrappe = wurzel / "ssh_attrappe.py"
attrappe.write_text(ATTRAPPE_PY, encoding="utf-8")
cmd = wurzel / "ssh_attrappe.cmd"
cmd.write_text(f'@"{sys.executable}" "{attrappe}" %*\r\n', encoding="utf-8")
modul = modul or lade()
modul.SSH = str(cmd)
# Abdichtung (Plankritik DH, 2026-09-26): Der echte WSL-Weg lief sonst gegen den echten
# Projektordner, fragte das Gateway und schickte echte ntfy-Meldungen. wsl.exe und das
# Skript zeigen ins Leere, und fahre_wsl ist eine Attrappe, die jeden Aufruf samt beiden
# Pfaden mitschreibt und ablehnt -- ausser ein Fall hat seine eigene gesetzt (wsl_attrappe).
modul.WSL = str(wurzel / "kein-wsl" / "wsl.exe")
modul.WSL_SKRIPT = "/nicht-vorhanden/abendkontrolle_wsl.py"
unerwartet = []
if not getattr(modul.fahre_wsl, "attrappe", False):
def wsl_verboten(tag, teil="alpaca"):
unerwartet.append((tag, teil, modul.WSL, modul.WSL_SKRIPT))
raise RuntimeError("echter WSL-Weg ohne Attrappe")
modul.fahre_wsl = wsl_verboten
os.environ["ANGRIFF_PROTOKOLL"] = str(protokoll)
os.environ["ANGRIFF_ART"] = art
code = modul.main(["--vault", str(vault), *argumente])
if unerwartet:
raise AssertionError(f"WSL-Weg ohne Attrappe gerufen: {unerwartet}")
notizen = sorted((vault / "Inbox").glob("*.md"))
notiz = "\n".join(n.read_text(encoding="utf-8") for n in notizen)
ablage = vault / modul.ABLAGE_TEIL
ergebnisse = sorted(p.name for p in ablage.glob("*.json")) if ablage.exists() else []
aufrufe = []
if protokoll.exists():
aufrufe = [json.loads(z) for z in protokoll.read_text(encoding="utf-8").splitlines() if z.strip()]
return code, notiz, ergebnisse, aufrufe
def wsl_attrappe(modul, gerufen):
"""Ersetzt fahre_wsl im Modul; schreibt (tag, teil) mit und liefert ein Ergebnis des Teils."""
def fahre_wsl(tag, teil="alpaca"):
gerufen.append((tag, teil))
return {"tag": tag, "teil": teil, "befunde": [], "hinweise": [], "fakten": {}, "exit": 0}
fahre_wsl.attrappe = True
modul.fahre_wsl = fahre_wsl
@fall
def ergebnis_vom_host_landet_in_ablage_und_notiz():
code, notiz, ergebnisse, aufrufe = lauf(
"gut", "--quelle", "dockerhost", "--tag", "2026-10-05", "--jetzt", "2026-10-05T23:30"
)
assert code == 0, code
assert ergebnisse == ["2026-10-05-ibkr.json", "2026-10-05.json"], ergebnisse
assert "Abendkontrolle Trading Plattform (automatisch): ohne Befund" in notiz, notiz
assert "Abendkontrolle IBKR (automatisch): ohne Befund" in notiz, notiz
assert len(aufrufe) == 2, aufrufe
@fall
def ssh_mit_festem_schluessel_ohne_rueckfrage_nur_zum_bekannten_host():
_, _, _, aufrufe = lauf("gut", "--quelle", "dockerhost", "--tag", "2026-10-05", "--jetzt", "2026-10-05T23:30")
for argumente, ziel in zip(aufrufe, ("2026-10-05", "2026-10-05-ibkr"), strict=True):
assert argumente[:3] == ["-p", "2498", "-i"], argumente
assert argumente[3].endswith("trading-dh-kontrolle"), argumente
assert argumente[4:14] == [
"-o", "BatchMode=yes",
"-o", "IdentitiesOnly=yes",
"-o", "StrictHostKeyChecking=yes",
"-o", "UserKnownHostsFile=~/.ssh/known_hosts_trading_dh",
"-o", "ConnectTimeout=20",
], argumente
assert argumente[14:] == ["[email protected]", ziel], argumente
@fall
def noch_kein_ergebnis_ist_ein_befund_in_der_notiz_und_wird_wiederholt():
code, notiz, ergebnisse, _ = lauf(
"fehlt", "--quelle", "dockerhost", "--tag", "2026-10-05", "--jetzt", "2026-10-05T23:30"
)
assert code == 1, code
assert ergebnisse == [], ergebnisse
assert "**gescheitert**" in notiz and "noch kein Ergebnis" in notiz, notiz
assert "#needs-review" in notiz, notiz
assert "fuer 2026-10-05-ibkr noch kein Ergebnis" in notiz, notiz
@fall
def muell_vom_host_ist_ein_befund():
code, notiz, ergebnisse, _ = lauf(
"muell", "--quelle", "dockerhost", "--tag", "2026-10-05", "--jetzt", "2026-10-05T23:30"
)
assert code == 1, code
assert ergebnisse == [], ergebnisse
assert "keine JSON-Zeile vom Docker-Host" in notiz, notiz
@fall
def faellige_tage_kommen_einzeln_ueber_ssh():
modul = lade()
_, _, ergebnisse, aufrufe = lauf("gut", "--quelle", "dockerhost", "--jetzt", "2026-10-05T22:10", modul=modul)
tage = ["2026-09-28", "2026-09-29", "2026-09-30", "2026-10-01", "2026-10-02"]
ibkr = [f"{t}-ibkr" for t in tage if t >= modul.IBKR_ERSTER_TAG.isoformat()]
ziele = [a[-1] for a in aufrufe]
assert ziele == tage + ibkr, ziele
assert ergebnisse == sorted(f"{z}.json" for z in ziele), ergebnisse
@fall
def ohne_schalter_bleibt_es_bei_wsl():
modul = lade()
gerufen = []
wsl_attrappe(modul, gerufen)
def verboten(*_argumente):
raise AssertionError("ohne --quelle darf nichts ueber SSH laufen")
modul.fahre_dockerhost = verboten
code, _, ergebnisse, aufrufe = lauf("gut", "--tag", "2026-10-05", "--jetzt", "2026-10-05T23:30", modul=modul)
assert code == 0, code
assert gerufen == [("2026-10-05", "alpaca"), ("2026-10-05", "ibkr")], gerufen
assert aufrufe == [], aufrufe
assert ergebnisse == ["2026-10-05-ibkr.json", "2026-10-05.json"], ergebnisse
@fall
def die_ibkr_instanz_bleibt_mit_quelle_ibkr_wsl_in_wsl():
modul = lade()
gerufen = []
wsl_attrappe(modul, gerufen)
code, _, ergebnisse, aufrufe = lauf(
"gut", "--quelle", "dockerhost", "--quelle-ibkr", "wsl", "--tag", "2026-10-05", "--jetzt", "2026-10-05T23:30",
modul=modul,
)
assert code == 0, code
assert [a[-1] for a in aufrufe] == ["2026-10-05"], aufrufe
assert gerufen == [("2026-10-05", "ibkr")], gerufen
assert ergebnisse == ["2026-10-05-ibkr.json", "2026-10-05.json"], ergebnisse
@fall
def ein_ergebnis_des_falschen_teils_vom_host_gilt_als_gescheitert():
code, notiz, ergebnisse, aufrufe = lauf(
"falscher_teil", "--quelle", "dockerhost", "--teil", "ibkr", "--tag", "2026-10-05", "--jetzt", "2026-10-05T23:30"
)
assert code == 1, code
assert [a[-1] for a in aufrufe] == ["2026-10-05-ibkr"], aufrufe
assert ergebnisse == [], ergebnisse
assert "gehoert zu 'alpaca'" in notiz and "**gescheitert**" in notiz, notiz
@fall
def eine_unbekannte_quelle_ist_ein_bedienfehler():
fehler = io.StringIO()
try:
with contextlib.redirect_stderr(fehler):
lauf("gut", "--quelle", "irgendwo", "--tag", "2026-10-05")
except SystemExit as ende:
assert ende.code == 2, ende.code
assert "--quelle: invalid choice" in fehler.getvalue(), fehler.getvalue()
return
raise AssertionError("argparse hat die Quelle irgendwo angenommen")
@fall
def die_heutige_frist_und_die_aufgabe_passen_zueinander():
quelle = (HIER / "abendkontrolle.py").read_text(encoding="utf-8")
aufgabe = (HIER / "abendkontrolle_einrichten.ps1").read_text(encoding="utf-8")
assert "StrictHostKeyChecking=no" not in quelle
assert "if tag == jetzt.date() and jetzt.time() < FRUEHESTENS:" in quelle
assert aufgabe.count('[ValidateSet("wsl", "dockerhost")]') == 2
assert "'\" --quelle ' + $Quelle + ' --quelle-ibkr ' + $QuelleIbkr" in aufgabe
assert 'if ($Quelle -eq "dockerhost" -or $QuelleIbkr -eq "dockerhost") { $Uhrzeit = "23:15" }' in aufgabe
assert "if (-not $QuelleIbkr) { $QuelleIbkr = $Quelle }" in aufgabe
@fall
def der_echte_wsl_weg_ist_versperrt():
# Ohne --quelle und ohne eigene Attrappe nimmt main den WSL-Weg. Er muss als Abweichung
# enden, nicht als Lauf.
try:
lauf("gut", "--tag", "2026-10-05", "--jetzt", "2026-10-05T23:30")
except AssertionError as ausnahme:
text = str(ausnahme)
assert "WSL-Weg ohne Attrappe" in text, text
# Beleg fuer die Zeilen in lauf(): Beim Aufruf zeigten wsl.exe und das Skript ins Leere.
assert "kein-wsl" in text and "/nicht-vorhanden/" in text, text
return
raise AssertionError("ein Lauf ohne WSL-Attrappe kam durch")
@fall
def der_echte_wsl_aufruf_findet_kein_wsl():
# Die zweite Sperre: Selbst die echte fahre_wsl findet in der Abdichtung kein wsl.exe, und das
# Skript, das sie dort starten wuerde, gibt es nicht.
modul = lade()
with tempfile.TemporaryDirectory() as ordner:
modul.WSL = str(Path(ordner) / "kein-wsl" / "wsl.exe")
modul.WSL_SKRIPT = "/nicht-vorhanden/abendkontrolle_wsl.py"
try:
modul.fahre_wsl("2026-10-05", "alpaca")
except FileNotFoundError:
return
raise AssertionError("fahre_wsl lief ohne wsl.exe weiter")
def main() -> int:
abweichungen = 0
for funktion in FAELLE:
try:
funktion()
except (Exception, SystemExit) as ausnahme:
abweichungen += 1
print(f"ABWEICHUNG {funktion.__name__}: {type(ausnahme).__name__}: {ausnahme}")
else:
print(f"OK {funktion.__name__}")
print(f"{len(FAELLE)} Faelle, {abweichungen} Abweichung(en) ({datetime.now():%Y-%m-%d %H:%M})")
return 2 if abweichungen else 0
if __name__ == "__main__":
sys.exit(main())