Files
felhom.eu/documentation/audits/lockouts-2026-10-01/tools/b_three.py
T

73 lines
4.5 KiB
Python

#!/usr/bin/env python3
"""R-752 on 9202 with the LIVE catalog templates (no change yet): BookStack, Grafana, calibre-web-automated.
Per app: install through the product, wait for the install hold to open, positive control, a stranger's wrong tries
with the PUBLIC login name, the household's right password right after, then how long until it works again, then
a wrong one still refused. One app at a time; each removed at the end."""
import sys, time
import walk as w
import lk
APPS = sys.argv[1:] or ["bookstack", "grafana", "calibre-web"]
SUB = {"bookstack": "wiki", "grafana": "grafana", "calibre-web": "books"}
KEY = {"bookstack": "ADMIN_PASSWORD", "grafana": "GF_SECURITY_ADMIN_PASSWORD", "calibre-web": "ADMIN_PASSWORD"}
NAME = {"bookstack": "admin@admin.com", "grafana": "admin", "calibre-web": "admin"}
PROBE = {"bookstack": lk.bookstack, "grafana": lk.grafana, "calibre-web": lk.calibre}
w.login()
for app in APPS:
sub, name, probe = SUB[app], NAME[app], PROBE[app]
lk.p(f"##### {app} — controller {w.guest('docker inspect felhom-controller --format {{.Config.Image}}').strip()}")
lk.p("deploy:", w.deploy(app, sub))
for _ in range(120):
time.sleep(5)
logs = w.guest(f"docker logs --since 20m felhom-controller 2>&1 | grep -E '{app}: install hold OPENED|{app}.*deployed successfully'")
if "hold OPENED" in logs or (app == "grafana" and "deployed successfully" in logs):
break
w.wait_app(sub, "/login", want=("200",), tries=60)
pw = (w.GENERATED.get(app) or {}).get(KEY[app], "")
lk.p("generated password length:", len(pw))
lk.p("positive control — right password:", probe(name, pw))
tries = 6 if app != "calibre-web" else 4
for i in range(1, tries + 1):
lk.p(f"stranger wrong try {i}:", probe(name, f"wrong-{i}-{time.time()}"))
lk.p("household, RIGHT password, right after:", probe(name, pw))
# calibre-web counts EVERY try (the household's too) against 3/minute and 40/day — poll it slowly
every, cap = (70, 6 * 60) if app == "calibre-web" else (15, 20 * 60)
waited = lk.wait_until(lambda: probe(name, pw), "ok", every, cap, "right password")
lk.p(f"RESULT {app}: the right password worked again after {waited if waited is None else round(waited, 1)} min")
lk.p("a wrong one after:", probe(name, "wrong-after"), "| right again:", probe(name, pw))
if app == "calibre-web":
lk.p("## calibre-web: OPDS basic auth (the book-feed door) — 60 wrong tries as a stranger")
res = [lk.calibre_opds(name, f"opds-wrong-{i}") for i in range(60)]
lk.p("OPDS wrong tries:", {r: res.count(r) for r in set(res)})
lk.p("OPDS right password after:", lk.calibre_opds(name, pw), "| the login form, right password:", lk.calibre(name, pw))
lk.p("## calibre-web: the DAILY limit — 40 wrong tries at the 3/minute pace (~14 min), then the right password")
n = 0
while n < 40:
for _ in range(3):
if n < 40:
n += 1
r = lk.calibre(name, f"daily-{n}")
if n % 10 == 0 or r not in ("wrong",):
lk.p(f" wrong try {n}: {r}")
time.sleep(61)
lk.p("household, RIGHT password after 40:", lk.calibre(name, pw))
time.sleep(130)
lk.p("household, RIGHT password 2 min later (past the minute window):", lk.calibre(name, pw))
lk.p("restart the app (the limiter's store is in memory):", w.guest("docker restart calibre-web >/dev/null && echo restarted").strip())
w.wait_app(sub, "/login", want=("200",), tries=60)
lk.p("household, RIGHT password after the restart:", lk.calibre(name, pw))
if app == "grafana":
lk.p("## grafana: the TRICKLE — 5 wrong, then one wrong try per minute for 8 minutes; the right password each minute")
for i in range(5):
lk.p(f"burst wrong {i + 1}:", probe(name, f"burst-{i}-{time.time()}"))
for m in range(8):
time.sleep(60)
lk.p(f" minute {m + 1}: stranger wrong -> {probe(name, f'trickle-{m}')}, household right -> {probe(name, pw)}")
lk.p("stop the trickle; wait for the right password:")
waited = lk.wait_until(lambda: probe(name, pw), "ok", 15, 10 * 60, "right password")
lk.p(f"RESULT grafana trickle: open again {waited if waited is None else round(waited, 1)} min after the last wrong try")
lk.p(w.guest(f"docker logs --since 30m {app} 2>&1 | grep -i -E 'lock|block|throttl|limit' | tail -4 | cut -c1-200").strip())
w.remove(app)
lk.p("removed; deployed =", w.stack(app).get("deployed"))