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 @@
-
+
@@ -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 @@
-
+
@@ -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