Korrektur Prüfskript für Missbrauchserkennung
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
1
tools/check-all.sh
Executable file → Normal 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
0
tools/check-env.sh
Executable file → Normal file
215
tools/check-imports.py
Normal file
215
tools/check-imports.py
Normal 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
0
tools/smoke-test.sh
Executable file → Normal file
Reference in New Issue
Block a user