From b00d1f08b56cb7f03abee7766fe8a1ef8d97b0c1 Mon Sep 17 00:00:00 2001 From: Hugo Date: Sun, 4 Oct 2026 12:13:45 +0200 Subject: [PATCH 1/2] fix(engine): resolve the project root before naming its files Module sources are loaded by their resolved path, while dependency files, configuration files, advisory files and the policy file were named from the root as given. With a relative root, their findings were reported relative to the working directory and their baseline fingerprints changed with it. Refs #206 --- src/coretrace_python/engine.py | 4 ++ tests/test_relative_root.py | 82 ++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 tests/test_relative_root.py diff --git a/src/coretrace_python/engine.py b/src/coretrace_python/engine.py index 98f63a8..b67c163 100644 --- a/src/coretrace_python/engine.py +++ b/src/coretrace_python/engine.py @@ -384,6 +384,10 @@ def analyze_project( if jobs < 1: raise ValueError("jobs must be at least 1") + # Module sources are loaded by their resolved path; every other file under the root + # (manifests, configuration, advisories, policy) must be named the same way, or the + # reports and baselines would see it relative to the working directory (#206). + root = root.resolve() sources = SourceManager() findings: list[Finding] = [] dependencies = resolve_dependencies(root, sources) diff --git a/tests/test_relative_root.py b/tests/test_relative_root.py new file mode 100644 index 0000000..fa8ccd7 --- /dev/null +++ b/tests/test_relative_root.py @@ -0,0 +1,82 @@ +"""Acceptance tests for issue #206: a root given as a relative path. + +Findings located in a dependency file or a configuration file are reported relative to +the analysed root, like the findings of Python modules, whatever the working directory +and whether the root was given as a relative or an absolute path. A baseline recorded +with one form of the root therefore matches the findings of a check with the other. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from coretrace_python.cli import main +from coretrace_python.dependency import Advisory, dump_advisories +from coretrace_python.findings import Severity + +LOCK = """\ +version = 1 + +[[package]] +name = "vulnlib" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +""" +ADVISORY = Advisory("CVE-2099-0206", "vulnlib", "<1.1", "vulnlib is vulnerable", Severity.MEDIUM) +SETTINGS = '{"api_token": "Zx81kQpLw0RtY7vBn3MsD9cF2hJ6gK4a"}\n' + + +@pytest.fixture +def project(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: + """A project ``proj`` under the working directory, with a vulnerable locked + requirement and a credential in a configuration file.""" + + root = tmp_path / "proj" + root.mkdir() + (root / "uv.lock").write_text(LOCK, encoding="utf-8") + (root / "advisories.json").write_text(dump_advisories((ADVISORY,)), encoding="utf-8") + (root / "settings.json").write_text(SETTINGS, encoding="utf-8") + (root / "app.py").write_text("import vulnlib\n", encoding="utf-8") + monkeypatch.chdir(tmp_path) + return root + + +def located(capsys: pytest.CaptureFixture[str], rule: str) -> list[str]: + report = json.loads(capsys.readouterr().out) + return sorted(f["location"]["path"] for f in report["findings"] if f["rule_id"] == rule) + + +@pytest.mark.parametrize("form", ["relative", "absolute"]) +def test_dependency_findings_are_located_relative_to_the_root( + project: Path, form: str, capsys: pytest.CaptureFixture[str] +) -> None: + root = "proj" if form == "relative" else str(project) + + main(["--check", root, "--format", "json"]) + + assert located(capsys, "vulnerable-dependency") == ["uv.lock"] + + +@pytest.mark.parametrize("form", ["relative", "absolute"]) +def test_configuration_findings_are_located_relative_to_the_root( + project: Path, form: str, capsys: pytest.CaptureFixture[str] +) -> None: + root = "proj" if form == "relative" else str(project) + + main(["--check", root, "--format", "json"]) + + assert located(capsys, "hardcoded-credential") == ["settings.json"] + + +def test_a_baseline_recorded_with_a_relative_root_matches_an_absolute_one( + project: Path, tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + baseline = tmp_path / "baseline.json" + assert main(["--check", "proj", "--baseline", str(baseline)]) == 0 + capsys.readouterr() + + assert main(["--check", str(project), "--baseline", str(baseline)]) == 0 + assert capsys.readouterr().out.startswith("no findings, 2 baselined\n") From 29306727486ef6313e44ca2293a67fce43a94a7f Mon Sep 17 00:00:00 2001 From: Hugo Date: Sun, 4 Oct 2026 13:01:04 +0200 Subject: [PATCH 2/2] fix(engine): resolve explicit advisory and policy files with the root The --advisories and --policy paths reached the source manager as given, so their syntax-error notes were still located relative to the working directory. Cover them and a root reached through a symbolic link. Refs #206 --- src/coretrace_python/engine.py | 8 +++++--- tests/test_relative_root.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/coretrace_python/engine.py b/src/coretrace_python/engine.py index b67c163..c951d6e 100644 --- a/src/coretrace_python/engine.py +++ b/src/coretrace_python/engine.py @@ -384,10 +384,12 @@ def analyze_project( if jobs < 1: raise ValueError("jobs must be at least 1") - # Module sources are loaded by their resolved path; every other file under the root - # (manifests, configuration, advisories, policy) must be named the same way, or the - # reports and baselines would see it relative to the working directory (#206). + # Module sources are loaded by their resolved path; every other file the project is + # read from (manifests, configuration, advisories, policy) must be named the same way, + # or the reports and baselines would see it relative to the working directory (#206). root = root.resolve() + advisory_files = [path.resolve() for path in advisory_files] + policy_file = None if policy_file is None else policy_file.resolve() sources = SourceManager() findings: list[Finding] = [] dependencies = resolve_dependencies(root, sources) diff --git a/tests/test_relative_root.py b/tests/test_relative_root.py index fa8ccd7..276e525 100644 --- a/tests/test_relative_root.py +++ b/tests/test_relative_root.py @@ -71,6 +71,35 @@ def test_configuration_findings_are_located_relative_to_the_root( assert located(capsys, "hardcoded-credential") == ["settings.json"] +@pytest.mark.parametrize( + ("option", "name", "text"), + [ + ("--policy", "bad-policy.toml", "[dependencies\n"), + ("--advisories", "bad-advisories.json", "{\n"), + ], +) +def test_explicit_files_are_located_relative_to_the_root( + project: Path, option: str, name: str, text: str, capsys: pytest.CaptureFixture[str] +) -> None: + (project / name).write_text(text, encoding="utf-8") + + main(["--check", "proj", option, f"proj/{name}", "--format", "json"]) + + assert located(capsys, "syntax-error") == [name] + + +@pytest.mark.parametrize("form", ["relative", "absolute"]) +def test_a_root_reached_through_a_symbolic_link_keeps_root_relative_paths( + project: Path, tmp_path: Path, form: str, capsys: pytest.CaptureFixture[str] +) -> None: + (tmp_path / "link").symlink_to(project, target_is_directory=True) + root = "link" if form == "relative" else str(tmp_path / "link") + + main(["--check", root, "--format", "json"]) + + assert located(capsys, "vulnerable-dependency") == ["uv.lock"] + + def test_a_baseline_recorded_with_a_relative_root_matches_an_absolute_one( project: Path, tmp_path: Path, capsys: pytest.CaptureFixture[str] ) -> None: