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.maintenance import describe, run_cleanup
|
||||||
from app.models import EmailChange, User
|
from app.models import EmailChange, User
|
||||||
from app.schemas import MailCheckOut, MailTestIn, MessageOut
|
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 (
|
from app.schemas_admin import (
|
||||||
AdminSettingsIn,
|
AdminSettingsIn,
|
||||||
AdminSettingsOut,
|
AdminSettingsOut,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ from app.schemas import (
|
|||||||
from app.runtime_settings import get_duration
|
from app.runtime_settings import get_duration
|
||||||
from app.users import apply_if_complete
|
from app.users import apply_if_complete
|
||||||
from app.security import (
|
from app.security import (
|
||||||
|
bucket_key,
|
||||||
check_rate_limit,
|
check_rate_limit,
|
||||||
hash_password,
|
hash_password,
|
||||||
hash_token,
|
hash_token,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ from app.schemas_shopping import (
|
|||||||
TransferOwnershipIn,
|
TransferOwnershipIn,
|
||||||
)
|
)
|
||||||
from app.security import (
|
from app.security import (
|
||||||
|
bucket_key,
|
||||||
check_rate_limit,
|
check_rate_limit,
|
||||||
hash_token,
|
hash_token,
|
||||||
new_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 "JavaScript-Module" node tools/check-js.mjs
|
||||||
run "Strichcode-Decoder" node tools/test-barcode.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-Syntax" sh -c 'cd backend && python3 -m compileall -q app alembic && echo "in Ordnung"'
|
||||||
|
run "Python-Namen" python3 tools/check-imports.py
|
||||||
|
|
||||||
printf '\n'
|
printf '\n'
|
||||||
if [ "$failed" -eq 0 ]; then
|
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