Korrektur Prüfskript für Missbrauchserkennung

This commit is contained in:
2026-08-13 20:58:47 +02:00
parent 1d295796b4
commit fa64cbc4a4
7 changed files with 224 additions and 1 deletions

View File

@@ -26,7 +26,12 @@ from app.mail import (
from app.maintenance import describe, run_cleanup
from app.models import EmailChange, User
from app.schemas import MailCheckOut, MailTestIn, MessageOut
from app.runtime_settings import BY_KEY, describe_durations, set_duration
from app.runtime_settings import (
BY_KEY,
describe_durations,
get_duration,
set_duration,
)
from app.schemas_admin import (
AdminSettingsIn,
AdminSettingsOut,

View File

@@ -31,6 +31,7 @@ from app.schemas import (
from app.runtime_settings import get_duration
from app.users import apply_if_complete
from app.security import (
bucket_key,
check_rate_limit,
hash_password,
hash_token,

View File

@@ -31,6 +31,7 @@ from app.schemas_shopping import (
TransferOwnershipIn,
)
from app.security import (
bucket_key,
check_rate_limit,
hash_token,
new_token,

1
tools/check-all.sh Executable file → Normal file
View File

@@ -35,6 +35,7 @@ run "nginx-Konfiguration" python3 tools/check-nginx.py web
run "JavaScript-Module" node tools/check-js.mjs
run "Strichcode-Decoder" node tools/test-barcode.mjs
run "Python-Syntax" sh -c 'cd backend && python3 -m compileall -q app alembic && echo "in Ordnung"'
run "Python-Namen" python3 tools/check-imports.py
printf '\n'
if [ "$failed" -eq 0 ]; then

0
tools/check-env.sh Executable file → Normal file
View File

215
tools/check-imports.py Normal file
View File

@@ -0,0 +1,215 @@
#!/usr/bin/env python3
"""Sucht Namen, die verwendet, aber nicht importiert oder definiert sind.
python3 tools/check-imports.py
Warum das nötig ist: `python3 -m compileall` prüft nur die Syntax. Ein
fehlender Import ist syntaktisch einwandfrei - der Fehler tritt erst auf,
wenn die betroffene Zeile tatsächlich ausgeführt wird:
NameError: name 'bucket_key' is not defined
Genau das ist passiert: Beim Umstellen der Missbrauchsbremse suchte eine
Textersetzung nach einem einzeiligen Import, der in zwei Dateien aber
mehrzeilig in Klammern stand. Der Aufruf wurde eingebaut, der Import
nicht - und aufgefallen ist es erst beim Anmeldeversuch im Betrieb.
Arbeitet über den abstrakten Syntaxbaum (`ast`), nicht über reguläre
Ausdrücke: Python bringt seinen eigenen Parser mit, also gibt es keinen
Grund zu raten.
Grenzen, die bewusst in Kauf genommen werden:
- Namen, die nur zur Laufzeit entstehen (globals(), setattr), werden
als unbekannt gemeldet
- Sternchen-Importe (from x import *) können nicht aufgelöst werden
"""
import ast
import builtins
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
APP = ROOT / "backend" / "app"
BUILTINS = set(dir(builtins)) | {
"__name__", "__file__", "__doc__", "__all__", "__spec__", "__package__",
"self", "cls",
}
class Scope:
"""Ein Geltungsbereich mit seinen bekannten Namen."""
def __init__(self, parent=None):
self.parent = parent
self.names: set[str] = set()
def add(self, name: str) -> None:
self.names.add(name)
def knows(self, name: str) -> bool:
if name in self.names:
return True
return self.parent.knows(name) if self.parent else False
class Checker(ast.NodeVisitor):
def __init__(self, path: Path):
self.path = path
self.module = Scope()
self.problems: list[tuple[int, str]] = []
# Erst alle Namen der Modulebene einsammeln, dann prüfen -
# eine Funktion darf etwas benutzen, das weiter unten steht.
self.pending: list[tuple[ast.AST, Scope]] = []
# ---- Namen einsammeln ----
def collect(self, node: ast.AST, scope: Scope) -> None:
for child in ast.iter_child_nodes(node):
if isinstance(child, (ast.Import, ast.ImportFrom)):
for alias in child.names:
scope.add((alias.asname or alias.name).split(".")[0])
elif isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef,
ast.ClassDef)):
scope.add(child.name)
elif isinstance(child, ast.Assign):
for target in child.targets:
self.bind(target, scope)
elif isinstance(child, (ast.AnnAssign, ast.AugAssign)):
self.bind(child.target, scope)
elif isinstance(child, (ast.For, ast.AsyncFor)):
self.bind(child.target, scope)
self.collect(child, scope)
elif isinstance(child, (ast.With, ast.AsyncWith)):
for item in child.items:
if item.optional_vars:
self.bind(item.optional_vars, scope)
self.collect(child, scope)
elif isinstance(child, ast.ExceptHandler):
if child.name:
scope.add(child.name)
self.collect(child, scope)
elif isinstance(child, (ast.If, ast.Try, ast.While)):
self.collect(child, scope)
elif isinstance(child, (ast.Global, ast.Nonlocal)):
for name in child.names:
scope.add(name)
def bind(self, target: ast.AST, scope: Scope) -> None:
if isinstance(target, ast.Name):
scope.add(target.id)
elif isinstance(target, (ast.Tuple, ast.List)):
for element in target.elts:
self.bind(element, scope)
elif isinstance(target, ast.Starred):
self.bind(target.value, scope)
# ---- Prüfen ----
def check_function(self, node, parent: Scope) -> None:
scope = Scope(parent)
args = node.args
for arg in (args.posonlyargs + args.args + args.kwonlyargs):
scope.add(arg.arg)
if args.vararg:
scope.add(args.vararg.arg)
if args.kwarg:
scope.add(args.kwarg.arg)
self.collect(node, scope)
# Alles, was IRGENDWO im Rumpf gebunden wird, gilt als bekannt:
# Schleifenvariablen in Comprehensions, Lambda-Parameter, späte
# Importe innerhalb der Funktion, Zuweisungen in verschachtelten
# Blöcken. Diese Prüfung soll fehlende Importe finden, nicht
# Geltungsbereiche im Detail nachbilden - dafür wäre ein echtes
# Namensauflösungsmodell nötig, und die Fehlalarme wären
# zahlreicher als die Funde.
bound = set(scope.names)
for child in ast.walk(node):
if isinstance(child, ast.Name) and isinstance(child.ctx, ast.Store):
bound.add(child.id)
elif isinstance(child, (ast.Import, ast.ImportFrom)):
for alias in child.names:
bound.add((alias.asname or alias.name).split(".")[0])
elif isinstance(child, ast.arg):
bound.add(child.arg)
elif isinstance(child, ast.ExceptHandler) and child.name:
bound.add(child.name)
elif isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef,
ast.ClassDef)):
bound.add(child.name)
for child in ast.walk(node):
if isinstance(child, ast.Name) and isinstance(child.ctx, ast.Load):
if child.id in bound or child.id in BUILTINS:
continue
if not scope.knows(child.id):
self.problems.append((child.lineno, child.id))
# Verschachtelte Funktionen einzeln
for child in ast.iter_child_nodes(node):
self.walk_definitions(child, scope)
def walk_definitions(self, node: ast.AST, scope: Scope) -> None:
for child in ast.walk(node):
if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
self.check_function(child, scope)
elif isinstance(child, ast.ClassDef):
inner = Scope(scope)
self.collect(child, inner)
for member in ast.iter_child_nodes(child):
if isinstance(member, (ast.FunctionDef, ast.AsyncFunctionDef)):
self.check_function(member, inner)
def run(self, tree: ast.Module) -> list[tuple[int, str]]:
self.collect(tree, self.module)
# Namen auf Modulebene
for child in ast.iter_child_nodes(tree):
if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
self.check_function(child, self.module)
elif isinstance(child, ast.ClassDef):
inner = Scope(self.module)
self.collect(child, inner)
for member in ast.iter_child_nodes(child):
if isinstance(member, (ast.FunctionDef, ast.AsyncFunctionDef)):
self.check_function(member, inner)
return self.problems
def main() -> int:
files = sorted(APP.rglob("*.py"))
total = 0
for path in files:
source = path.read_text(encoding="utf-8")
if "import *" in source:
continue
try:
tree = ast.parse(source)
except SyntaxError as exc:
print(f" {path.relative_to(ROOT)}: Syntaxfehler in Zeile {exc.lineno}")
total += 1
continue
problems = Checker(path).run(tree)
if problems:
print(f"\n {path.relative_to(ROOT)}")
for line, name in sorted(set(problems)):
print(f" Zeile {line}: '{name}' wird verwendet, "
"aber nirgends importiert oder definiert")
total += 1
if total:
print(f"\n{total} nicht aufgelöste(r) Name(n)")
return 1
print(f"{len(files)} Python-Dateien geprüft, alle Namen aufgelöst")
return 0
if __name__ == "__main__":
raise SystemExit(main())

0
tools/smoke-test.sh Executable file → Normal file
View File