From d6ce6f6607ce58e7422431f28618c55034051f8f Mon Sep 17 00:00:00 2001 From: Arjun K <130126062+arjun-k047@users.noreply.github.com> Date: Tue, 20 Oct 2026 15:59:20 +0530 Subject: [PATCH] MAINT: P311-19 hash pinned classifier datasets with hashlib.file_digest compute_dataset_hashes() on LocalViolenceClassifierScorer and LocalRefusalClassifierScorer hashed each pinned training CSV by reading the whole file into memory first. It now streams each file through hashlib.file_digest, so the digest no longer needs the full contents in memory. The returned mapping is unchanged. _load_training_rows() keeps hashing the bytes it already read, because it parses the same bytes it verifies; reading the file a second time would split the check from the content used for training. Co-Authored-By: Claude Opus 5.5 --- .../float_scale/local_violence_classifier_scorer.py | 9 +++++---- .../score/true_false/local_refusal_classifier_scorer.py | 9 +++++---- tests/unit/score/test_local_refusal_classifier_scorer.py | 9 +++++++++ .../unit/score/test_local_violence_classifier_scorer.py | 9 +++++++++ 4 files changed, 28 insertions(+), 8 deletions(-) diff --git a/pyrit/score/float_scale/local_violence_classifier_scorer.py b/pyrit/score/float_scale/local_violence_classifier_scorer.py index b0bb721da6..b35c06cd58 100644 --- a/pyrit/score/float_scale/local_violence_classifier_scorer.py +++ b/pyrit/score/float_scale/local_violence_classifier_scorer.py @@ -547,10 +547,11 @@ def compute_dataset_hashes() -> dict[str, str]: dict[str, str]: File name to current SHA-256, for re-pinning after a deliberate dataset update. """ - return { - file_name: hashlib.sha256((_HARM_EVALS_PATH / file_name).read_bytes()).hexdigest() - for file_name in _TRAINING_DATASETS - } + hashes: dict[str, str] = {} + for file_name in _TRAINING_DATASETS: + with (_HARM_EVALS_PATH / file_name).open("rb") as dataset_file: + hashes[file_name] = hashlib.file_digest(dataset_file, "sha256").hexdigest() + return hashes def _build_identifier(self) -> ComponentIdentifier: """ diff --git a/pyrit/score/true_false/local_refusal_classifier_scorer.py b/pyrit/score/true_false/local_refusal_classifier_scorer.py index 4d900b81dc..f8b50aaf6e 100644 --- a/pyrit/score/true_false/local_refusal_classifier_scorer.py +++ b/pyrit/score/true_false/local_refusal_classifier_scorer.py @@ -554,10 +554,11 @@ def compute_dataset_hashes() -> dict[str, str]: dict[str, str]: File name to current SHA-256, for re-pinning after a deliberate dataset update. """ - return { - file_name: hashlib.sha256((_REFUSAL_EVALS_PATH / file_name).read_bytes()).hexdigest() - for file_name in _TRAINING_DATASETS - } + hashes: dict[str, str] = {} + for file_name in _TRAINING_DATASETS: + with (_REFUSAL_EVALS_PATH / file_name).open("rb") as dataset_file: + hashes[file_name] = hashlib.file_digest(dataset_file, "sha256").hexdigest() + return hashes def _build_identifier(self) -> ComponentIdentifier: """ diff --git a/tests/unit/score/test_local_refusal_classifier_scorer.py b/tests/unit/score/test_local_refusal_classifier_scorer.py index 913580d19b..dc7e9ee498 100644 --- a/tests/unit/score/test_local_refusal_classifier_scorer.py +++ b/tests/unit/score/test_local_refusal_classifier_scorer.py @@ -57,6 +57,15 @@ def test_training_dataset_pins() -> None: assert LocalRefusalClassifierScorer.compute_dataset_hashes() == module._TRAINING_DATASETS +def test_compute_dataset_hashes_reads_current_files(tmp_path: Path) -> None: + contents = {file_name: f"{file_name}\n".encode() * 50_000 for file_name in module._TRAINING_DATASETS} + for file_name, data in contents.items(): + (tmp_path / file_name).write_bytes(data) + with patch.object(module, "_REFUSAL_EVALS_PATH", tmp_path): + hashes = LocalRefusalClassifierScorer.compute_dataset_hashes() + assert hashes == {file_name: hashlib.sha256(data).hexdigest() for file_name, data in contents.items()} + + def test_training_rejects_changed_data(tmp_path: Path) -> None: objective = "context " * 2000 response = "response " * 1000 diff --git a/tests/unit/score/test_local_violence_classifier_scorer.py b/tests/unit/score/test_local_violence_classifier_scorer.py index a02715fcc3..e4d77ad15a 100644 --- a/tests/unit/score/test_local_violence_classifier_scorer.py +++ b/tests/unit/score/test_local_violence_classifier_scorer.py @@ -268,6 +268,15 @@ def test_compute_dataset_hashes_matches_pins() -> None: assert LocalViolenceClassifierScorer.compute_dataset_hashes() == module._TRAINING_DATASETS +def test_compute_dataset_hashes_reads_current_files(tmp_path: Path) -> None: + contents = {file_name: f"{file_name}\n".encode() * 50_000 for file_name in module._TRAINING_DATASETS} + for file_name, data in contents.items(): + (tmp_path / file_name).write_bytes(data) + with patch.object(module, "_HARM_EVALS_PATH", tmp_path): + hashes = LocalViolenceClassifierScorer.compute_dataset_hashes() + assert hashes == {file_name: hashlib.sha256(data).hexdigest() for file_name, data in contents.items()} + + class _Tokenizer: """Tiny tokenizer with visible token IDs and the pinned encoder's framing budget."""