From b0e8161918bc0965d69ce55b8344e37af87d5330 Mon Sep 17 00:00:00 2001 From: Grovy311 Date: Sat, 17 Oct 2026 11:34:13 +0200 Subject: [PATCH] fix(security): mutation_check ohne veralteten Bytecode, ruff-fest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- scripts/security/mutation_check.py | 57 ++++++++++++++++++++---------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/scripts/security/mutation_check.py b/scripts/security/mutation_check.py index f91d86f..1e77eb5 100644 --- a/scripts/security/mutation_check.py +++ b/scripts/security/mutation_check.py @@ -18,7 +18,7 @@ Format (Pfade relativ zur TOML-Datei): file = "backend/app/main.py" old = "if project is None or not can_access(user, project):" new = "if project is None:" -""" +""" # noqa: E501, RUF100 from __future__ import annotations @@ -28,12 +28,24 @@ import sys import tomllib from pathlib import Path -# Python prüft .pyc nur über Quell-mtime (Sekunden) und Größe. Eine gleich lange Mutation, -# die in derselben Sekunde geschrieben oder zurückgesetzt wird, liefe sonst mit dem alten -# Bytecode: Mutation wirkt nicht bzw. bleibt nach dem Lauf aktiv (llm-server, 2026-10-17). +# Python prüft .pyc nur über Quell-mtime (Sekunden) und Größe. Eine gleich lange +# Mutation, in derselben Sekunde geschrieben oder zurückgesetzt, liefe sonst mit altem +# Bytecode: sie wirkt nicht oder bleibt nach dem Lauf aktiv (llm-server, 2026-10-17). 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: target.write_text(text, encoding="utf-8") if target.suffix == ".py": @@ -41,6 +53,16 @@ def _write(target: Path, text: str) -> None: pyc.unlink(missing_ok=True) +def _failed_lines(stdout: str) -> list[str]: + # pytest: "FAILED ...", node:test: "✖ " (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: base = config_path.parent config = tomllib.loads(config_path.read_text(encoding="utf-8")) @@ -52,38 +74,35 @@ def run(config_path: Path) -> int: return 1 # 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: - print("Tests sind schon ohne Mutation rot – erst reparieren.\n") + print("Tests sind schon ohne Mutation rot, erst reparieren.\n") print(baseline.stdout[-3000:]) return 1 survivors = [] for mutation in mutations: + name = mutation["name"] target = base / mutation["file"] original = target.read_text(encoding="utf-8") count = original.count(mutation["old"]) if count != 1: - print(f"[FEHLER] {mutation['name']}: Suchtext {count}x gefunden (erwartet 1) – Mutation veraltet") - survivors.append(mutation["name"]) + print(f"[FEHLER] {name}: Suchtext {count}x gefunden (erwartet 1), veraltet") + survivors.append(name) continue try: _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: _write(target, original) if result.returncode == 0: - print(f"[ÜBERLEBT] {mutation['name']}: Tests bleiben grün") - survivors.append(mutation["name"]) + print(f"[ÜBERLEBT] {name}: Tests bleiben grün") + survivors.append(name) else: - # pytest: "FAILED ...", node:test: "✖ " (Suiten und Zusammenfassung ausblenden) - failed = [ - line.strip() - for line in result.stdout.splitlines() - 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}") + failed = _failed_lines(result.stdout) + hint = "Tests rot, Ursache prüfen (Syntaxfehler?):\n" + reason = failed[0] if failed else hint + result.stdout[-800:] + print(f"[GETÖTET] {name}: {reason}") print(f"\n{len(mutations) - len(survivors)}/{len(mutations)} Mutationen erkannt.") return 1 if survivors else 0