fix(security): mutation_check ohne veralteten Bytecode, ruff-fest
Some checks are pending
security-baseline / secrets (push) Waiting to run
security-baseline / node (push) Waiting to run
security-baseline / semgrep (push) Waiting to run
security-baseline / trivy (push) Waiting to run

Stand cj/security-baseline: PYTHONDONTWRITEBYTECODE und Cache-Löschung nach jedem
Schreiben (gleich lange Mutationen liefen sonst mit altem .pyc), Testaufruf so
umbrochen, dass noqa/nosec bei 88 Zeichen auf der Aufrufzeile bleiben.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Grovy311 2026-10-17 11:34:13 +02:00
parent a20b6230db
commit b0e8161918

View file

@ -18,7 +18,7 @@ Format (Pfade relativ zur TOML-Datei):
file = "backend/app/main.py" file = "backend/app/main.py"
old = "if project is None or not can_access(user, project):" old = "if project is None or not can_access(user, project):"
new = "if project is None:" new = "if project is None:"
""" """ # noqa: E501, RUF100
from __future__ import annotations from __future__ import annotations
@ -28,12 +28,24 @@ import sys
import tomllib import tomllib
from pathlib import Path from pathlib import Path
# Python prüft .pyc nur über Quell-mtime (Sekunden) und Größe. Eine gleich lange Mutation, # Python prüft .pyc nur über Quell-mtime (Sekunden) und Größe. Eine gleich lange
# die in derselben Sekunde geschrieben oder zurückgesetzt wird, liefe sonst mit dem alten # Mutation, in derselben Sekunde geschrieben oder zurückgesetzt, liefe sonst mit altem
# Bytecode: Mutation wirkt nicht bzw. bleibt nach dem Lauf aktiv (llm-server, 2026-10-17). # Bytecode: sie wirkt nicht oder bleibt nach dem Lauf aktiv (llm-server, 2026-10-17).
TEST_ENV = {**os.environ, "PYTHONDONTWRITEBYTECODE": "1"} TEST_ENV = {**os.environ, "PYTHONDONTWRITEBYTECODE": "1"}
def _run_tests(test_cmd: list[str], workdir: Path) -> subprocess.CompletedProcess:
# Startet die Tests der App ohne Shell, Kommando aus deren Mutations-TOML.
return subprocess.run( # noqa: S603, RUF100 # nosec B603
test_cmd,
cwd=workdir,
capture_output=True,
text=True,
env=TEST_ENV,
check=False,
)
def _write(target: Path, text: str) -> None: def _write(target: Path, text: str) -> None:
target.write_text(text, encoding="utf-8") target.write_text(text, encoding="utf-8")
if target.suffix == ".py": if target.suffix == ".py":
@ -41,6 +53,16 @@ def _write(target: Path, text: str) -> None:
pyc.unlink(missing_ok=True) pyc.unlink(missing_ok=True)
def _failed_lines(stdout: str) -> list[str]:
# pytest: "FAILED ...", node:test: "✖ <Testname>" (ohne Suiten und Zusammenfassung)
lines = []
for line in stdout.splitlines():
node_fail = line.startswith(" ✖") and "failing tests" not in line
if line.startswith("FAILED") or node_fail:
lines.append(line.strip())
return lines
def run(config_path: Path) -> int: def run(config_path: Path) -> int:
base = config_path.parent base = config_path.parent
config = tomllib.loads(config_path.read_text(encoding="utf-8")) config = tomllib.loads(config_path.read_text(encoding="utf-8"))
@ -52,38 +74,35 @@ def run(config_path: Path) -> int:
return 1 return 1
# Vorbedingung: ohne Mutation muss alles grün sein, sonst beweist Rot nichts. # Vorbedingung: ohne Mutation muss alles grün sein, sonst beweist Rot nichts.
baseline = subprocess.run(test_cmd, cwd=workdir, capture_output=True, text=True, env=TEST_ENV) # noqa: S603 # nosec B603 baseline = _run_tests(test_cmd, workdir)
if baseline.returncode != 0: if baseline.returncode != 0:
print("Tests sind schon ohne Mutation rot – erst reparieren.\n") print("Tests sind schon ohne Mutation rot, erst reparieren.\n")
print(baseline.stdout[-3000:]) print(baseline.stdout[-3000:])
return 1 return 1
survivors = [] survivors = []
for mutation in mutations: for mutation in mutations:
name = mutation["name"]
target = base / mutation["file"] target = base / mutation["file"]
original = target.read_text(encoding="utf-8") original = target.read_text(encoding="utf-8")
count = original.count(mutation["old"]) count = original.count(mutation["old"])
if count != 1: if count != 1:
print(f"[FEHLER] {mutation['name']}: Suchtext {count}x gefunden (erwartet 1) – Mutation veraltet") print(f"[FEHLER] {name}: Suchtext {count}x gefunden (erwartet 1), veraltet")
survivors.append(mutation["name"]) survivors.append(name)
continue continue
try: try:
_write(target, original.replace(mutation["old"], mutation["new"])) _write(target, original.replace(mutation["old"], mutation["new"]))
result = subprocess.run(test_cmd, cwd=workdir, capture_output=True, text=True, env=TEST_ENV) # noqa: S603 # nosec B603 result = _run_tests(test_cmd, workdir)
finally: finally:
_write(target, original) _write(target, original)
if result.returncode == 0: if result.returncode == 0:
print(f"[ÜBERLEBT] {mutation['name']}: Tests bleiben grün") print(f"[ÜBERLEBT] {name}: Tests bleiben grün")
survivors.append(mutation["name"]) survivors.append(name)
else: else:
# pytest: "FAILED ...", node:test: "✖ <Testname>" (Suiten und Zusammenfassung ausblenden) failed = _failed_lines(result.stdout)
failed = [ hint = "Tests rot, Ursache prüfen (Syntaxfehler?):\n"
line.strip() reason = failed[0] if failed else hint + result.stdout[-800:]
for line in result.stdout.splitlines() print(f"[GETÖTET] {name}: {reason}")
if line.startswith("FAILED") or (line.startswith(" ✖") and "failing tests" not in line)
]
reason = failed[0] if failed else "Tests rot – Ursache prüfen (Syntaxfehler?):\n" + result.stdout[-800:]
print(f"[GETÖTET] {mutation['name']}: {reason}")
print(f"\n{len(mutations) - len(survivors)}/{len(mutations)} Mutationen erkannt.") print(f"\n{len(mutations) - len(survivors)}/{len(mutations)} Mutationen erkannt.")
return 1 if survivors else 0 return 1 if survivors else 0