fix(security): mutation_check ohne veralteten Bytecode, ruff-fest
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:
parent
a20b6230db
commit
b0e8161918
1 changed files with 38 additions and 19 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue