Files
felhom.eu/documentation/audits/rehearsal-2026-10-20/tools/gen_household.py
T
admin f84f94d987 Rehearsal 2026-10-20 (Tester4) + data-loss hunt: baselines, B1 install evidence, A6 drawn order, tools, first two reviewers' candidates
No release, deploy or publish. Off-site is OFF for Tester4 and the owner passphrase is needed from the operator (bind blocked after 2 of 5 tries).
2026-10-20 11:57:55 +02:00

327 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/python3
"""gen_household.py — the pretend household's data for the rehearsal of 2026-10-20 (brief A5).
SYNTHETIC ONLY: no real person, photo or document. Seed 20261020 — re-running gives the same bytes
(except the films' container timestamps, which ffmpeg is told to zero with -fflags +bitexact).
Writes everything under OUT (default /root/data) and, LAST, OUT/MANIFEST.tsv:
path<TAB>size<TAB>sha256<TAB>app<TAB>class<TAB>note
class: A = an app whose FILES go off-site (immich, nextcloud, paperless-ngx; 07 §6.2 'mandatory' legs)
B = an app whose data lives in Docker volumes (vaultwarden, mealie, actual, radicale, bookstack)
M = media library binds (jellyfin films) — in NO tier, must never go off-site (07 §6)
No password is generated here — the family's passwords are minted on DooPlex straight into `sec`.
usage: gen_household.py [OUT]
"""
import datetime as dt
import hashlib
import json
import os
import random
import shutil
import subprocess
import sys
import unicodedata
from PIL import Image, ImageDraw, ImageFont
SEED = 20261020
OUT = sys.argv[1] if len(sys.argv) > 1 else "/root/data"
R = random.Random(SEED)
FONT = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
PEOPLE = [("odon", "Kovács Ödön"), ("irisz", "Szűts Írisz"), ("kata", "Kovács Kata")]
manifest = [] # (relpath, app, cls, note)
def p(*parts):
path = os.path.join(OUT, *parts)
os.makedirs(os.path.dirname(path), exist_ok=True)
return path
def add(path, app, cls, note=""):
manifest.append((os.path.relpath(path, OUT), app, cls, note))
# ---------------------------------------------------------------- photos (immich, class A)
def photos():
font = ImageFont.truetype(FONT, 64)
small = ImageFont.truetype(FONT, 28)
places = [("Balaton", 46.90, 17.89), ("Budapest", 47.4979, 19.0402), ("Eger", 47.9025, 20.3772),
("Pécs", 46.0727, 18.2323), ("Tokaj", 48.1186, 21.4089)]
for i in range(300):
year = 2009 + i % 18
when = dt.datetime(year, R.randint(1, 12), R.randint(1, 28), R.randint(7, 20), R.randint(0, 59), R.randint(0, 59))
w, h = (1600, 1200) if i % 3 else (1200, 1600)
img = Image.new("RGB", (w, h))
d = ImageDraw.Draw(img)
c1 = (R.randint(0, 255), R.randint(0, 255), R.randint(0, 255))
c2 = (R.randint(0, 255), R.randint(0, 255), R.randint(0, 255))
for y in range(0, h, 4):
t = y / h
d.rectangle([0, y, w, y + 4], fill=tuple(int(c1[k] * (1 - t) + c2[k] * t) for k in range(3)))
for _ in range(40): # shapes so the JPEG is not trivially small and every photo differs
x0, y0 = R.randint(0, w), R.randint(0, h)
d.ellipse([x0, y0, x0 + R.randint(20, 300), y0 + R.randint(20, 300)],
fill=(R.randint(0, 255), R.randint(0, 255), R.randint(0, 255)))
place = places[i % len(places)]
d.text((60, 60), f"Családi fotó #{i:03d}", font=font, fill="white")
d.text((60, 150), f"{when:%Y.%m.%d} — {place[0]} — szintetikus tesztkép", font=small, fill="white")
path = p("immich", f"{year}", f"IMG_{year}{i:04d}.jpg")
img.save(path, "JPEG", quality=88)
args = ["exiftool", "-q", "-overwrite_original", f"-DateTimeOriginal={when:%Y:%m:%d %H:%M:%S}",
f"-CreateDate={when:%Y:%m:%d %H:%M:%S}", "-Make=Felhom-rehearsal", "-Model=synthetic"]
if i % 3 == 0: # a third carry GPS
args += [f"-GPSLatitude={place[1]}", "-GPSLatitudeRef=N", f"-GPSLongitude={place[2]}", "-GPSLongitudeRef=E"]
subprocess.run(args + [path], check=True)
add(path, "immich", "A", f"exif {when:%Y-%m-%d}" + (" gps" if i % 3 == 0 else ""))
# two exact duplicates of photo 7 under other names
src = p("immich", "2016", "IMG_20160007.jpg")
for n in ("IMG_20160007_masolat.jpg", "duplikatum_IMG_20160007.jpg"):
dst = p("immich", "edge", n)
shutil.copyfile(src, dst)
add(dst, "immich", "A", "exact duplicate of 2016/IMG_20160007.jpg")
# a truncated (broken) JPEG
raw = open(p("immich", "2020", "IMG_20200011.jpg"), "rb").read()
path = p("immich", "edge", "torott_kep.jpg")
open(path, "wb").write(raw[: len(raw) // 3])
add(path, "immich", "A", "truncated JPEG (first third)")
# a zero-byte file
path = p("immich", "edge", "ures.jpg")
open(path, "wb").close()
add(path, "immich", "A", "zero bytes")
# a PNG
path = p("immich", "edge", "rajz_Kata.png")
img = Image.new("RGB", (800, 600), (250, 240, 200))
ImageDraw.Draw(img).text((40, 40), "Kata rajza — árvíztűrő tükörfúrógép", font=small, fill="black")
img.save(path, "PNG")
add(path, "immich", "A", "PNG")
# a short video (<= 150 MB)
path = p("immich", "edge", "nyaralas_2025.mp4")
subprocess.run(["ffmpeg", "-loglevel", "error", "-y", "-f", "lavfi", "-i", "testsrc2=size=1920x1080:rate=30",
"-f", "lavfi", "-i", "sine=frequency=440:sample_rate=48000", "-t", "75",
"-c:v", "libx264", "-preset", "ultrafast", "-b:v", "12M", "-c:a", "aac", "-shortest",
"-fflags", "+bitexact", "-metadata", "creation_time=2025-07-14T10:00:00Z", path], check=True)
add(path, "immich", "A", "video ~75 s")
# ---------------------------------------------------------------- documents (paperless-ngx, class A)
def documents():
from reportlab.lib.pagesizes import A4
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfgen import canvas
pdfmetrics.registerFont(TTFont("DejaVu", FONT))
sellers = ["Mátra Villamosság Kft.", "Fővárosi Vízművek (teszt)", "Tisza Gázszolgáltató Zrt. (teszt)",
"Kovács és Társa Bt.", "Zöldség-Gyümölcs Kisbolt"]
for i in range(50):
path = p("paperless", f"szamla_{2024 + i % 3}_{i:03d}.pdf")
c = canvas.Canvas(path, pagesize=A4, invariant=1)
c.setFont("DejaVu", 18)
c.drawString(60, 780, f"Számla — {R.choice(sellers)}")
c.setFont("DejaVu", 11)
y = 740
for line in [f"Számlaszám: TESZT-{SEED}-{i:04d}", f"Vevő: {PEOPLE[i % 3][1]}",
f"Kelt: {2024 + i % 3}.{1 + i % 12:02d}.{1 + i % 27:02d}",
"Tétel: árvíztűrő tükörfúrógép (szintetikus)", f"Összeg: {R.randint(2, 900) * 100} Ft",
"Fizetési mód: átutalás", "Ez egy tesztszámla, valódi ügylet nem tartozik hozzá.",
"ŐŰ őű ÁÉÍÓÖÚÜ áéíóöúü — ékezetpróba"]:
c.drawString(60, y, line)
y -= 18
c.showPage()
c.save()
add(path, "paperless", "A", "Hungarian text invoice")
# image-only PDF (no text layer — OCR must work)
img = Image.new("RGB", (1240, 1754), "white")
d = ImageDraw.Draw(img)
f = ImageFont.truetype(FONT, 40)
d.text((100, 150), "Szkennelt nyilatkozat", font=f, fill="black")
d.text((100, 250), "Alulírott Kovács Ödön kijelentem, hogy", font=f, fill="black")
d.text((100, 320), "ez egy szintetikus, kép alapú PDF.", font=f, fill="black")
path = p("paperless", "szkennelt_nyilatkozat.pdf")
img.save(path, "PDF", resolution=150)
add(path, "paperless", "A", "image-only PDF (OCR)")
# password-protected PDF (the app cannot open it without the password)
plain = p("paperless", "_tmp_jelszavas.pdf")
c = canvas.Canvas(plain, pagesize=A4, invariant=1)
c.setFont("DejaVu", 14)
c.drawString(60, 780, "Jelszóval védett bankszámlakivonat (szintetikus)")
c.save()
path = p("paperless", "jelszavas_kivonat.pdf")
# the PDF's own password is a fixed test literal, not a household secret
subprocess.run(["qpdf", "--encrypt", "teszt-pdf-jelszo", "teszt-pdf-jelszo", "256", "--", plain, path], check=True)
os.remove(plain)
add(path, "paperless", "A", "password-protected PDF")
# a 100-page PDF
path = p("paperless", "kezikonyv_100_oldal.pdf")
c = canvas.Canvas(path, pagesize=A4, invariant=1)
for pg in range(1, 101):
c.setFont("DejaVu", 12)
c.drawString(60, 800, f"Háztartási kézikönyv — {pg}. oldal")
for k in range(40):
c.drawString(60, 770 - k * 18, f"{pg}.{k}: Ügyelj a fűtésre, a vízórára és a biztosítékokra (teszt).")
c.showPage()
c.save()
add(path, "paperless", "A", "100 pages")
# ---------------------------------------------------------------- files (nextcloud, class A)
def files():
base = p("nextcloud", "Csalad", ".keep")
os.remove(base) if os.path.exists(base) else None
root = os.path.dirname(base)
# a tree 12 folders deep with ~3000 small files
n = 0
for top in range(6):
d = os.path.join(root, f"Archívum_{top}")
for depth in range(12):
d = os.path.join(d, f"szint{depth:02d}")
os.makedirs(d, exist_ok=True)
for k in range(42):
path = os.path.join(d, f"jegyzet_{top}_{depth}_{k}.txt")
with open(path, "w", encoding="utf-8") as fh:
fh.write(f"Jegyzet {top}/{depth}/{k} — {R.random():.12f} — őűáé\n" * R.randint(1, 30))
add(path, "nextcloud", "A", "tree file")
n += 1
# 1.5 GB of pseudorandom bytes (no dedup cheats), deterministic from the seed via AES-CTR
path = os.path.join(root, "nagy_fajl_1_5GB.bin")
key = hashlib.sha256(f"felhom-rehearsal-{SEED}".encode()).hexdigest()
with open(path, "wb") as fh:
subprocess.run(f"head -c 1610612736 /dev/zero | openssl enc -aes-256-ctr -nosalt -K {key} -iv "
f"{'0' * 32}", shell=True, check=True, stdout=fh)
add(path, "nextcloud", "A", "1.5 GiB random")
edge = os.path.join(root, "Kulonleges_nevek")
os.makedirs(edge, exist_ok=True)
name = "Ödön születésnapja – fénykép lista.txt"
for form in ("NFC", "NFD"):
path = os.path.join(edge, unicodedata.normalize(form, name))
with open(path, "w", encoding="utf-8") as fh:
fh.write(f"Ugyanaz a név, {form} formában (Mac kliens NFD-t küld).\n")
add(path, "nextcloud", "A", f"accented name in {form}")
stem = "hosszu_nev_" + "ő" * 115 # 11 + 230 bytes
long_name = (stem + ".txt").encode()
assert len(long_name) <= 255, len(long_name)
path = os.path.join(edge, long_name.decode())
open(path, "w").write(f"name is {len(long_name)} bytes in UTF-8\n")
add(path, "nextcloud", "A", f"name {len(long_name)} bytes")
for nm, note in (("-kezdo_kotojel.txt", "leading dash"), ("Kata's lista szóközökkel.txt", "spaces and quote")):
path = os.path.join(edge, nm)
open(path, "w").write(note + "\n")
add(path, "nextcloud", "A", note)
for nm, when in (("regi_1980.txt", dt.datetime(1980, 1, 2, 12, 0)), ("jovo_2030.txt", dt.datetime(2030, 6, 1, 12, 0))):
path = os.path.join(edge, nm)
open(path, "w").write(f"mtime {when:%Y-%m-%d}\n")
ts = when.replace(tzinfo=dt.timezone.utc).timestamp()
os.utime(path, (ts, ts))
add(path, "nextcloud", "A", f"mtime {when:%Y}")
path = os.path.join(edge, "csak_olvashato.txt")
open(path, "w").write("read-only\n")
os.chmod(path, 0o444)
add(path, "nextcloud", "A", "read-only (0444)")
link = os.path.join(edge, "link_a_nagy_fajlra")
if os.path.lexists(link):
os.remove(link)
os.symlink("../nagy_fajl_1_5GB.bin", link)
manifest.append((os.path.relpath(link, OUT), "nextcloud", "A", "SYMLINK -> ../nagy_fajl_1_5GB.bin"))
# ---------------------------------------------------------------- small apps (class B)
def small_apps():
logins = []
for i in range(50):
site = R.choice(["bank", "posta", "ugyfelkapu-teszt", "webshop", "iskola", "konyvtar", "szolgaltato"])
logins.append({"name": f"{site}-{i:02d}", "uri": f"https://{site}{i}.example.invalid/",
"username": f"{PEOPLE[i % 3][0]}{i}@example.invalid",
"folder": "Közös" if i < 10 else PEOPLE[i % 3][0]})
path = p("vaultwarden", "logins.json")
json.dump(logins, open(path, "w", encoding="utf-8"), ensure_ascii=False, indent=1)
add(path, "vaultwarden", "B", "50 logins (passwords generated at upload, not stored here)")
for k in range(2):
path = p("vaultwarden", f"melleklet_{k}.pdf")
Image.new("RGB", (400, 300), (k * 100, 50, 150)).save(path, "PDF")
add(path, "vaultwarden", "B", "attachment")
recipes = [
{"name": "Gulyásleves", "ingredients": ["50 dkg marhalábszár", "2 fej vöröshagyma", "pirospaprika", "burgonya",
"sárgarépa", "fehérrépa", "kömény"],
"steps": ["A hagymát zsíron megpirítjuk.", "Hozzáadjuk a húst és a paprikát.", "Felöntjük vízzel, főzzük."]},
{"name": "Csirkepörkölt", "ingredients": ["1 egész csirke", "2 fej hagyma", "1 paprika", "1 paradicsom",
"pirospaprika", "só"],
"steps": ["A hagymát üvegesre pirítjuk.", "Rátesszük a csirkét.", "Lassú tűzön puhára pároljuk."]}]
for r in recipes:
path = p("mealie", r["name"] + ".json")
json.dump(r, open(path, "w", encoding="utf-8"), ensure_ascii=False, indent=1)
add(path, "mealie", "B", "recipe")
path = p("mealie", r["name"] + ".jpg")
img = Image.new("RGB", (1024, 768), (180, 60, 30))
ImageDraw.Draw(img).text((40, 40), r["name"], font=ImageFont.truetype(FONT, 72), fill="white")
img.save(path, "JPEG", quality=85)
add(path, "mealie", "B", "recipe photo")
path = p("actual", "tranzakciok.csv")
with open(path, "w", encoding="utf-8") as fh:
fh.write("Date,Payee,Notes,Amount\n")
for i in range(200):
d = dt.date(2026, 1, 1) + dt.timedelta(days=i % 290)
amt = -R.randint(5, 4000) * 100 if i % 7 else R.randint(300, 800) * 1000
fh.write(f"{d.isoformat()},{R.choice(['Közért', 'Benzinkút', 'Gyógyszertár', 'Fizetés', 'Rezsi'])},"
f"teszt tétel {i},{amt}\n")
add(path, "actual", "B", "200 HUF transactions")
path = p("radicale", "csalad.ics")
with open(path, "w", encoding="utf-8", newline="\r\n") as fh:
fh.write("BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//felhom rehearsal//EN\n"
"BEGIN:VTIMEZONE\nTZID:Europe/Budapest\n"
"BEGIN:DAYLIGHT\nTZOFFSETFROM:+0100\nTZOFFSETTO:+0200\nTZNAME:CEST\nDTSTART:19700329T020000\n"
"RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\nEND:DAYLIGHT\n"
"BEGIN:STANDARD\nTZOFFSETFROM:+0200\nTZOFFSETTO:+0100\nTZNAME:CET\nDTSTART:19701025T030000\n"
"RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\nEND:STANDARD\nEND:VTIMEZONE\n"
"BEGIN:VEVENT\nUID:rehearsal-dst-1@felhom\nDTSTAMP:20261020T000000Z\n"
"DTSTART;TZID=Europe/Budapest:20261022T023000\nDTEND;TZID=Europe/Budapest:20261022T030000\n"
"RRULE:FREQ=DAILY;COUNT=7\nSUMMARY:Gyógyszer — éjjel 2:30 (DST-próba)\nEND:VEVENT\n"
"BEGIN:VEVENT\nUID:rehearsal-weekly-1@felhom\nDTSTAMP:20261020T000000Z\n"
"DTSTART;TZID=Europe/Budapest:20261018T090000\nDTEND;TZID=Europe/Budapest:20261018T100000\n"
"RRULE:FREQ=WEEKLY;COUNT=4\nSUMMARY:Vasárnapi ebéd a nagyiéknál\nEND:VEVENT\nEND:VCALENDAR\n")
add(path, "radicale", "B", "recurring event crossing DST 2026-10-25")
for i in range(5):
path = p("bookstack", f"oldal_{i}.md")
open(path, "w", encoding="utf-8").write(
f"# Családi wiki {i}. oldal\n\nA wifi jelszava a hűtőn van (teszt). Ékezetek: árvíztűrő tükörfúrógép.\n")
add(path, "bookstack", "B", "wiki page")
path = p("bookstack", f"kep_{i}.png")
Image.new("RGB", (640, 480), (20 * i, 120, 200)).save(path, "PNG")
add(path, "bookstack", "B", "wiki image")
# ---------------------------------------------------------------- media (jellyfin, class M)
def media():
for k, (title, pattern) in enumerate((("Csaladi_film_1", "testsrc"), ("Csaladi_film_2", "smptebars"))):
path = p("jellyfin", "Filmek", f"{title} (2026)", f"{title} (2026).mkv")
subprocess.run(["ffmpeg", "-loglevel", "error", "-y", "-f", "lavfi", "-i", f"{pattern}=size=1920x1080:rate=25",
"-f", "lavfi", "-i", "anoisesrc=color=pink:seed=%d" % (SEED + k), "-t", "540",
"-c:v", "libx264", "-preset", "ultrafast", "-b:v", "14M", "-maxrate", "14M", "-bufsize", "28M",
"-x264-params", "nal-hrd=cbr", "-c:a", "aac", "-shortest", "-fflags", "+bitexact", path], check=True)
add(path, "jellyfin", "M", "film ~1 GB — must never go off-site")
def write_manifest():
path = os.path.join(OUT, "MANIFEST.tsv")
with open(path, "w", encoding="utf-8") as fh:
fh.write("path\tsize\tsha256\tapp\tclass\tnote\n")
for rel, app, cls, note in manifest:
full = os.path.join(OUT, rel)
if os.path.islink(full):
fh.write(f"{rel}\t-\t-\t{app}\t{cls}\t{note}\n")
continue
h = hashlib.sha256()
with open(full, "rb") as f:
for chunk in iter(lambda: f.read(1 << 20), b""):
h.update(chunk)
fh.write(f"{rel}\t{os.path.getsize(full)}\t{h.hexdigest()}\t{app}\t{cls}\t{note}\n")
print(f"manifest: {len(manifest)} entries -> {path}")
if __name__ == "__main__":
os.makedirs(OUT, exist_ok=True)
for step in (photos, documents, files, small_apps, media):
t = dt.datetime.now()
step()
print(f"{step.__name__}: done in {(dt.datetime.now() - t).seconds} s, {len(manifest)} entries so far", flush=True)
write_manifest()