"""Mekanisk sjekk av Python-filer. Bare standardbiblioteket. python sjekk.py ordre.py [flere filer ...] Finner tre ting som ikke krever skjønn: - hemmeligheter skrevet rett i koden (navn som nøkkel, token, passord) - SQL bygd med tekst (f-streng, + eller .format rundt SQL-ord) - variabler som får en verdi og aldri brukes Avslutter med kode 1 hvis den finner noe. I en git-hook før commit og i CI stopper det, for der stopper alt som ikke er 0. Som Claude Code-hook ville kode 1 IKKE stoppet noe: der blokkerer bare kode 2, og kode 1 regnes som en feil som ikke blokkerer (code.claude.com/docs/en/hooks, lest 9. oktober 2026). Skriptet er laget for git og CI. Det er hele poenget: en regel som MÅ holde, skal ikke avhenge av at noen husker å be om den. Det som krever skjønn, som om koden gjør det spesifikasjonen sier, er jobben til skillene. Filer som ikke slutter på .py, hoppes over. En .py-fil som ikke kan tolkes, teller som et funn, så en commit ikke slipper gjennom fordi sjekken krasjet. Sjekken er bevisst enkel og vil både bomme og ta feil. Den erstatter ikke en ordentlig skanner for hemmeligheter eller en linter. Den viser prinsippet. """ import ast import pathlib import re import sys HEMMELIG_NAVN = re.compile(r"(n[oø]kkel|key|secret|hemmelig|passord|password|token)", re.I) SQL_ORD = re.compile(r"\b(SELECT|INSERT|UPDATE|DELETE)\b", re.I) def tekst_i(node) -> str: """Den bokstavelige teksten i en streng-node, også delene av en f-streng.""" return " ".join(n.value for n in ast.walk(node) if isinstance(n, ast.Constant) and isinstance(n.value, str)) def er_bygd_tekst(node) -> bool: if isinstance(node, ast.JoinedStr): return any(isinstance(v, ast.FormattedValue) for v in node.values) if isinstance(node, ast.BinOp) and isinstance(node.op, (ast.Add, ast.Mod)): return True if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute): return node.func.attr == "format" return False def sjekk_fil(sti: pathlib.Path): # utf-8-sig tåler filer som starter med et BOM-merke, slik noen editorer lagrer dem. try: tre = ast.parse(sti.read_text(encoding="utf-8-sig"), filename=str(sti)) except (SyntaxError, UnicodeDecodeError, ValueError) as feil: return [(getattr(feil, "lineno", None) or 0, "FEIL", f"kunne ikke tolkes: {type(feil).__name__}")] funn = [] for node in ast.walk(tre): # Hemmeligheter: et navn som ser hemmelig ut, satt til en lang tekst if isinstance(node, ast.Assign) and isinstance(node.value, ast.Constant) \ and isinstance(node.value.value, str) and len(node.value.value) >= 8: for mål in node.targets: if isinstance(mål, ast.Name) and HEMMELIG_NAVN.search(mål.id): funn.append((node.lineno, "SIKKERHET", f"hemmelighet i koden: {mål.id}")) # SQL bygd med tekst if er_bygd_tekst(node) and SQL_ORD.search(tekst_i(node)): funn.append((node.lineno, "SIKKERHET", "SQL bygd med tekst, bruk parametre (?)")) # Ubrukte variabler, én funksjon om gangen. En indre funksjon blir gått # gjennom både alene og som del av den ytre, derfor set() nederst. if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): satt, brukt = {}, set() for n in ast.walk(node): if isinstance(n, ast.Name): if isinstance(n.ctx, ast.Store): satt.setdefault(n.id, n.lineno) else: brukt.add(n.id) for navn, linje in satt.items(): if navn not in brukt and not navn.startswith("_"): funn.append((linje, "OPPRYDDING", f"«{navn}» får en verdi og brukes aldri")) return sorted(set(funn)) def main(filer) -> int: totalt = 0 for f in filer: sti = pathlib.Path(f) if sti.suffix != ".py": print(f"{sti.name}: hoppet over, ikke en Python-fil") continue for linje, type_, tekst in sjekk_fil(sti): print(f"{sti.name}:{linje:<4} {type_:<11} {tekst}") totalt += 1 print(f"{totalt} funn." if totalt else "Ingen funn. Det betyr bare at disse tre tingene ikke ble funnet.") return 1 if totalt else 0 if __name__ == "__main__": sys.stdout.reconfigure(encoding="utf-8") sys.exit(main(sys.argv[1:]))