diff --git a/MethodenAnalyser3.py b/MethodenAnalyser3.py index a459af6..5e8d18a 100644 --- a/MethodenAnalyser3.py +++ b/MethodenAnalyser3.py @@ -2456,11 +2456,23 @@ def _emit_cli_report(report: str) -> None: sys.stdout.write("\n") -def _write_cli_json_if_requested(report: dict[str, Any], output_path: str | None) -> bool: +def _write_cli_json_if_requested( + report: dict[str, Any], output_path: str | None, + protected_paths: list[str] | None = None, +) -> bool: """Schreibt optional den JSON-Report; Fehler gehen nach stderr.""" if not output_path: return True try: + target = os.path.normcase(os.path.realpath(output_path)) + for source in protected_paths or []: + same_path = target == os.path.normcase(os.path.realpath(source)) + same_file = ( + os.path.exists(output_path) and os.path.exists(source) + and os.path.samefile(output_path, source) + ) + if same_path or same_file: + raise ValueError("JSON-Ausgabe darf keine analysierte Quelldatei ersetzen.") written_path = write_json_report(report, output_path) except Exception as exc: # noqa: BLE001 -- CLI boundary reports export failures and returns a failure result print(f"[FEHLER] {_t('cli_json_export_failed')}: {exc}", file=sys.stderr) @@ -2479,7 +2491,7 @@ def _run_cli_file(path: str, json_output: str | None = None) -> int: _emit_cli_report(generate_report(result)) json_report = build_json_report("file", result, source_name=path) - if not _write_cli_json_if_requested(json_report, json_output): + if not _write_cli_json_if_requested(json_report, json_output, [path]): return EXIT_ANALYSIS_ERROR return EXIT_FINDINGS if _file_has_findings(result) else EXIT_OK @@ -2498,7 +2510,8 @@ def _run_cli_project(path: str, json_output: str | None = None) -> int: _emit_cli_report(generate_project_report(result, project_name=_extract_project_display_name(path))) json_report = build_json_report("project", result, source_name=path) - if not _write_cli_json_if_requested(json_report, json_output): + protected_paths = list(result.file_results) + [p for p, _ in result.files_with_errors] + if not _write_cli_json_if_requested(json_report, json_output, protected_paths): return EXIT_ANALYSIS_ERROR if result.files_with_errors: diff --git a/README.md b/README.md index 728cb6b..093de0b 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@

Version 3.0.2 CI Status - Tests 100% Green + Tests 100% Green Python 3.10-3.13 Cross-Platform GUI Desktop + CLI + PWA @@ -230,7 +230,7 @@ type path\to\file.py | python MethodenAnalyser3.py --stdin --json-output snippet python MethodenAnalyser3.py --lang en --file path/to/file.py ``` -The `--json-output` flag exports a machine-readable report named `methodenanalyser-report-v1.json` (or a custom name if specified). Its structure is documented in [EXPORTFORMAT.md](EXPORTFORMAT.md). +The `--json-output` flag exports a machine-readable report named `methodenanalyser-report-v1.json` (or a custom name if specified). Its structure is documented in [EXPORTFORMAT.md](EXPORTFORMAT.md). Export targets that refer to an analyzed source file, including symbolic links and hard links, are rejected with exit code 1 before writing. This also protects project files that failed analysis. The local `POST /api/analyze` helper accepts `source_kind` `snippet`, `file`, and `zip`. A `project` POST is intentionally rejected; project reports are diff --git a/README_de.md b/README_de.md index a8e85b1..767b25d 100644 --- a/README_de.md +++ b/README_de.md @@ -5,7 +5,7 @@

Version 3.0.2 CI-Status - Tests 100% Grün + Tests 100% Grün Python 3.10-3.13 Plattformübergreifend GUI Desktop + CLI + PWA @@ -230,7 +230,7 @@ type pfad\zur\datei.py | python MethodenAnalyser3.py --stdin --json-output snipp python MethodenAnalyser3.py --lang de --file pfad/zur/datei.py ``` -Das Flag `--json-output` exportiert einen maschinenlesbaren Bericht namens `methodenanalyser-report-v1.json` (oder unter dem übergebenen Dateinamen). Das Schema ist in [EXPORTFORMAT.md](EXPORTFORMAT.md) beschrieben. +Das Flag `--json-output` exportiert einen maschinenlesbaren Bericht namens `methodenanalyser-report-v1.json` (oder unter dem übergebenen Dateinamen). Das Schema ist in [EXPORTFORMAT.md](EXPORTFORMAT.md) beschrieben. Ausgabeziele, die auf eine analysierte Quelldatei zeigen, werden vor dem Schreiben mit Exit-Code 1 abgelehnt; das gilt auch für symbolische Links und Hard Links sowie für Projektdateien mit Analysefehlern. Der lokale `POST /api/analyze`-Endpunkt akzeptiert `source_kind` `snippet`, `file` und `zip`. Ein `project`-POST wird bewusst abgewiesen; Projektberichte entstehen diff --git a/llms.txt b/llms.txt index 8098dbc..b3af981 100644 --- a/llms.txt +++ b/llms.txt @@ -23,6 +23,7 @@ MethodenAnalyser analyzes Python files and project folders locally with Python A - CLI file analysis: `python MethodenAnalyser3.py --file path/to/file.py`. - CLI project analysis: `python MethodenAnalyser3.py --project path/to/project`. - JSON export: add `--json-output` or pass a target JSON filename. +- CLI JSON export rejects targets that alias analyzed sources (including hard links, symbolic links, and project files with analysis errors); exit code 1, no source write. This check does not lock against concurrent path changes. - Local web helper: `python webapp/server.py` or `START_WEBAPP.bat`; its API accepts `snippet`, `file` and `zip`, while `project` is import-only. - Multi-language support: `--lang` parameter supporting `de`, `en`, `es`, `zh`, `ja`, and `ru`. diff --git a/tests/test_json_source_guard.py b/tests/test_json_source_guard.py new file mode 100644 index 0000000..4aaf41f --- /dev/null +++ b/tests/test_json_source_guard.py @@ -0,0 +1,35 @@ +import os + +import pytest + +import MethodenAnalyser3 as app + + +@pytest.mark.parametrize("mode", ["file", "project"]) +@pytest.mark.parametrize("alias", ["direct", "hardlink", "symlink"]) +def test_cli_json_cannot_replace_analyzed_source(tmp_path, mode, alias): + source = tmp_path / "source.py" + original = b"import os\n" + source.write_bytes(original) + output = source + if alias != "direct": + output = tmp_path / "alias.json" + try: + if alias == "hardlink": + os.link(source, output) + else: + output.symlink_to(source) + except OSError as exc: + pytest.skip(f"{alias} unavailable: {exc}") + runner = app._run_cli_file if mode == "file" else app._run_cli_project + assert runner(str(source if mode == "file" else tmp_path), str(output)) == app.EXIT_ANALYSIS_ERROR + assert source.read_bytes() == original + assert output.read_bytes() == original + + +def test_project_json_cannot_replace_source_with_syntax_error(tmp_path): + source = tmp_path / "broken.py" + original = b"def invalid(:\n" + source.write_bytes(original) + assert app._run_cli_project(str(tmp_path), str(source)) == app.EXIT_ANALYSIS_ERROR + assert source.read_bytes() == original