- make quick/check/mutation from cj/security-baseline (Makefile unchanged, settings in baseline.mk), pre-commit, gitleaks, Semgrep rules, ESLint security rules, Forgejo workflow - tests/security: generic baseline tests plus app config and role tests against createApp() with an in-memory database - extract createApp() from src/server/index.ts; LB_DATABASE_PATH for tests - fix gaps found by the tests: reject unknown fields on project create, answer malformed JSON with 400 instead of 500, set nosniff on the API - npm audit fix (next 16.4.1 and others, no high/critical left) - clear ESLint errors, safeFileName without control-character regex - per-project ownership stays an open, documented gap (idorGap -> TODO) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
83 lines
3.2 KiB
Python
83 lines
3.2 KiB
Python
#!/usr/bin/env python3
|
||
"""Mutationsprüfung für Sicherheitstests.
|
||
|
||
Baut gezielt je eine Schutzfunktion aus (Textersetzung), lässt die Tests laufen und
|
||
erwartet ROT. Danach wird die Datei immer wiederhergestellt. Überlebt eine Mutation
|
||
(Tests bleiben grün), prüfen die Tests diese Schutzfunktion nicht wirklich.
|
||
|
||
Aufruf: mutation_check.py <mutations.toml>
|
||
Exit: 0 = jede Mutation hat mindestens einen Test rot gemacht, 1 = sonst
|
||
|
||
Format (Pfade relativ zur TOML-Datei):
|
||
|
||
test_cmd = [".venv/bin/python", "-m", "pytest", "-q", "-x", "-p", "no:cacheprovider"]
|
||
workdir = "backend"
|
||
|
||
[[mutation]]
|
||
name = "Objektberechtigung entfernt"
|
||
file = "backend/app/main.py"
|
||
old = "if project is None or not can_access(user, project):"
|
||
new = "if project is None:"
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import subprocess # nosec B404 - Tests starten ist der Zweck des Skripts
|
||
import sys
|
||
import tomllib
|
||
from pathlib import Path
|
||
|
||
|
||
def run(config_path: Path) -> int:
|
||
base = config_path.parent
|
||
config = tomllib.loads(config_path.read_text(encoding="utf-8"))
|
||
test_cmd = config["test_cmd"]
|
||
workdir = base / config.get("workdir", ".")
|
||
mutations = config.get("mutation", [])
|
||
if not mutations:
|
||
print("Keine Mutationen definiert.")
|
||
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) # noqa: S603 # nosec B603
|
||
if baseline.returncode != 0:
|
||
print("Tests sind schon ohne Mutation rot – erst reparieren.\n")
|
||
print(baseline.stdout[-3000:])
|
||
return 1
|
||
|
||
survivors = []
|
||
for mutation in mutations:
|
||
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"])
|
||
continue
|
||
try:
|
||
target.write_text(original.replace(mutation["old"], mutation["new"]), encoding="utf-8")
|
||
result = subprocess.run(test_cmd, cwd=workdir, capture_output=True, text=True) # noqa: S603 # nosec B603
|
||
finally:
|
||
target.write_text(original, encoding="utf-8")
|
||
if result.returncode == 0:
|
||
print(f"[ÜBERLEBT] {mutation['name']}: Tests bleiben grün")
|
||
survivors.append(mutation["name"])
|
||
else:
|
||
# pytest: "FAILED ...", node:test: "✖ <Testname>" (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}")
|
||
|
||
print(f"\n{len(mutations) - len(survivors)}/{len(mutations)} Mutationen erkannt.")
|
||
return 1 if survivors else 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
if len(sys.argv) != 2:
|
||
print(__doc__)
|
||
sys.exit(2)
|
||
sys.exit(run(Path(sys.argv[1]).resolve()))
|