#!/usr/bin/env python3 """Punkt 7: http:// -> https:// w polach source/sources/url receptur. - Podmienia TYLKO wystapienia zweryfikowane jako SAFE_HTTPS / SAFE_HTTPS_UNVERIFIED. - Granica dopasowania: po URL musi byc cudzyslow/bialy znak/przecinek/nawias/koniec, zeby nie trafic w prefiks dluzszego URL-a (np. url: 'http://x/' + source http://x/files/...). - Jesli ten sam URL wystepuje tez w build/hooks, plik obslugiwany jest zakresowo (tylko blok klucza), a nie globalnie. - Zachowuje CRLF (odczyt/zapis z newline=''). - Waliduje YAML przed i po; sprawdza, ze zmienily sie dokladnie oczekiwane wartosci. """ import collections import json import os import re import sys import yaml R = "/var/lib/pagan-sync/recipes" AUDIT = "/tmp/https-audit.json" CONV = ("SAFE_HTTPS", "SAFE_HTTPS_UNVERIFIED") LIST_KEYS = ("source", "sources") SCALAR_KEYS = ("url",) APPLY = "--apply" in sys.argv def as_list(v): if v is None: return [] return v if isinstance(v, list) else [v] def url_pat(url): return re.compile(re.escape(url) + r'(?=["\'\s\],]|$)') def block_span(text, key, top_only=True): """Zwraca (start, end) zakresu bloku listy dla klucza (razem z linia klucza).""" m = re.search(r"(?m)^%s:" % re.escape(key), text) if not m: return None line_end = text.find("\n", m.start()) if line_end == -1: line_end = len(text) rest = text[m.end():line_end].replace("\r", "").strip() if rest and not rest.startswith("["): return (m.start(), line_end) # skalarny/nie-listowy pos = line_end + 1 end = line_end item_indent = None while pos < len(text): nl = text.find("\n", pos) if nl == -1: nl = len(text) raw = text[pos:nl].replace("\r", "") if raw.strip() == "": break mi = re.match(r"^(\s*)-\s", raw) if mi: if item_indent is None or len(mi.group(1)) == item_indent: item_indent = len(mi.group(1)) end = nl pos = nl + 1 continue if raw[0] in " \t": end = nl pos = nl + 1 continue break return (m.start(), end) def in_other_value(d, urls): """Czy ktorys URL wystepuje w build/hooks (poza polami zrodel)?""" hits = [] for k in ("build", "hooks", "pre-install", "post-install", "prepare"): v = d.get(k) if v is None: continue s = json.dumps(v) if not isinstance(v, str) else v for u in urls: if u in s: hits.append((k, u)) return hits def main(): audit = json.load(open(AUDIT)) occ = [i for c in CONV for i in audit.get(c, [])] by_file = collections.defaultdict(list) for i in occ: by_file[i["recipe"]].append(i) changed_files = 0 changed_occ = 0 problems = [] scoped_files = [] report = [] for rel in sorted(by_file): path = os.path.join(R, rel) text = open(path, newline="", encoding="utf-8").read() try: before = yaml.safe_load(text) or {} except Exception as e: problems.append(f"{rel}: YAML przed nie parsuje sie: {e}") continue targets = by_file[rel] urls = [t["raw"] for t in targets] hits = in_other_value(before, urls) new = text applied = 0 if hits: scoped_files.append((rel, hits)) # tryb zakresowy: podmieniaj tylko w blokach kluczy for key in LIST_KEYS + SCALAR_KEYS: key_urls = [t["raw"] for t in targets if t["key"] == key] if not key_urls: continue sp = block_span(new, key) if not sp: problems.append(f"{rel}: zakresowy, brak bloku {key}") continue s, e = sp seg = new[s:e] for u in key_urls: seg2, n = url_pat(u).subn("https://" + u[7:], seg, count=1) if n: applied += n else: problems.append(f"{rel}: zakresowo nie znaleziono {u}") seg = seg2 new = new[:s] + seg + new[e:] else: for t in targets: new2, n = url_pat(t["raw"]).subn("https://" + t["raw"][7:], new, count=1) if n: applied += n else: problems.append(f"{rel}: nie znaleziono doslownie {t['raw']}") new = new2 if applied == 0: continue try: after = yaml.safe_load(new) or {} except Exception as e: problems.append(f"{rel}: YAML PO edycji nie parsuje sie: {e}") continue # walidacja: zmienily sie dokladnie oczekiwane wartosci ok = True for key in LIST_KEYS + SCALAR_KEYS: ob, oa = before.get(key), after.get(key) if ob == oa: continue wb, wa = as_list(ob), as_list(oa) if len(wb) != len(wa): problems.append(f"{rel}: {key} zmieniona dlugosc listy") ok = False continue for i, (x, y) in enumerate(zip(wb, wa)): if x == y: continue expect = str(x).replace("http://", "https://", 1) if str(y) != expect: problems.append(f"{rel}: {key}[{i}] nieoczekiwana zmiana: {x!r} -> {y!r}") ok = False # zadne inne klucze nie moga sie zmienic for k in set(before) | set(after): if k in LIST_KEYS + SCALAR_KEYS: continue if before.get(k) != after.get(k): problems.append(f"{rel}: nieoczekiwana zmiana klucza {k}") ok = False if not ok: continue if APPLY: with open(path, "w", newline="", encoding="utf-8") as f: f.write(new) changed_files += 1 changed_occ += applied report.append(f"{'ZMIENIONO' if APPLY else 'DRY'}: {rel} ({applied})") for line in report: print(line) print() print(f"plikow: {changed_files} | wystapien: {changed_occ} | tryb: {'APPLY' if APPLY else 'DRY-RUN'}") if scoped_files: print(f"\npliki z trybem zakresowym (URL tez w build/hooks): {len(scoped_files)}") for rel, hits in scoped_files: print(f" {rel}: {hits}") if problems: print(f"\nPROBLEMY ({len(problems)}):") for p in problems: print(" !", p) if __name__ == "__main__": sys.exit(main())