From 0b45d761d64672bb36783f2aee8ef28c2c07950e Mon Sep 17 00:00:00 2001 From: Hugo Date: Tue, 22 Sep 2026 00:39:58 +0200 Subject: [PATCH 1/2] test: define that every plugin file takes part in the cache key --- tests/test_cache.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/test_cache.py b/tests/test_cache.py index 4612bf8..011396e 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -13,6 +13,7 @@ from __future__ import annotations import json +import shutil from pathlib import Path import pytest @@ -119,6 +120,25 @@ def test_keys_depend_on_dependency_files(tmp_path: Path) -> None: assert all(after[name] != before[name] for name in before) +def test_keys_depend_on_every_plugin_file(tmp_path: Path) -> None: + root = standard(tmp_path) + plugins = tmp_path / "plugins" + shutil.copytree( + PLUGINS / "syntax" / "dangerous_eval", + plugins / "dangerous_eval", + ignore=shutil.ignore_patterns("__pycache__"), + ) + before = engine.analyze_project(root, [plugins]).keys + (plugins / "dangerous_eval" / "rules.json").write_text('{"v": 1}', encoding="utf-8") + after = engine.analyze_project(root, [plugins]).keys + (plugins / "dangerous_eval" / "__pycache__").mkdir(exist_ok=True) + (plugins / "dangerous_eval" / "__pycache__" / "x.pyc").write_bytes(b"\0") + with_bytecode = engine.analyze_project(root, [plugins]).keys + + assert all(after[name] != before[name] for name in before) + assert with_bytecode == after + + # --------------------------------------------------------------------------- reuse From c8fd3b994437d766cb9742928d940bff5d37d272 Mon Sep 17 00:00:00 2001 From: Hugo Date: Tue, 22 Sep 2026 00:39:58 +0200 Subject: [PATCH 2/2] fix: fingerprint every file of a plugin directory for the cache key directory_fingerprint hashed .py and .toml files only, so a plugin that ships its rules or advisories as .json or .yaml could change without invalidating cached results. Every regular file now takes part, except bytecode and hidden files. Closes #94 --- CHANGELOG.md | 6 ++++++ docs/usage.md | 5 +++-- src/coretrace_python/cache.py | 17 ++++++++++------- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 312ca68..366d5dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Plugins + +- The cache key covers every file of a plugin directory, not only `.py` and `.toml`, so rules or advisories shipped as data invalidate cached results when they change (#94). + ## 0.2.0 (2026-09-06) ### Adoption in an existing repository diff --git a/docs/usage.md b/docs/usage.md index 1e60046..d22046f 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -283,8 +283,9 @@ coretrace-python-analyzer --check src/ --sbom sbom.json --policy security/policy ## Large projects `--cache DIR` keeps the results of a directory check on disk, one entry per module, -keyed by the module's source, the tool and plugin versions, the security models, the -advisories, the dependency graph and the modules it imports. On the next run an unchanged +keyed by the module's source, the tool version, every file of every plugin directory +(code, manifest and data alike), the security models, the advisories, the dependency +graph and the modules it imports. On the next run an unchanged module is served from the cache, so editing one file re-analyses that file and its importers only. Entries are plain data; an unreadable entry is recomputed. diff --git a/src/coretrace_python/cache.py b/src/coretrace_python/cache.py index a0331c7..c2ec61a 100644 --- a/src/coretrace_python/cache.py +++ b/src/coretrace_python/cache.py @@ -14,7 +14,7 @@ import hashlib import json import os -from collections.abc import Iterable, Mapping +from collections.abc import Mapping from dataclasses import dataclass from pathlib import Path from types import MappingProxyType @@ -64,15 +64,18 @@ def fingerprint(*parts: str) -> str: return digest.hexdigest() -def directory_fingerprint(directory: Path, suffixes: Iterable[str] = (".py", ".toml")) -> str: - """A digest of the source files under ``directory``, so edited plugin code misses.""" +def directory_fingerprint(directory: Path) -> str: + """A digest of every file under ``directory``, so an edited plugin misses: its code, + its manifest and the rule or advisory data it ships in any format. Bytecode and + hidden files are not part of what the plugin does.""" - wanted = tuple(suffixes) parts: list[str] = [] for path in sorted(directory.rglob("*")): - if path.is_file() and path.suffix in wanted: - parts.append(str(path.relative_to(directory))) - parts.append(path.read_text(encoding="utf-8", errors="replace")) + relative = path.relative_to(directory) + if not path.is_file() or any(p == "__pycache__" or p.startswith(".") for p in relative.parts): + continue + parts.append(str(relative)) + parts.append(path.read_bytes().hex()) return fingerprint(*parts)