Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Bug Fixes

- Keep incomplete Joblib NumPy-wrapper scans failed closed when embedded pickle analysis also reports warnings.
- Avoid network false positives for bounded README examples that download sample images over HTTPS from Hugging Face.
- Prevent Windows cache identity probes from creating locked temporary files inside scanned directories.
- Preserve locked Windows cache probes reached through directory aliases while clearing stale scan results.
Expand Down
9 changes: 1 addition & 8 deletions modelaudit/scanners/joblib_scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -1530,15 +1530,8 @@ def scan(self, path: str) -> ScanResult:
result.finish(success=False)
return result

has_security_findings = any(
issue.severity in {IssueSeverity.WARNING, IssueSeverity.CRITICAL} for issue in result.issues
)
has_trusted_incomplete_tail = result.metadata.get("trusted_incomplete_tail") is True
if (
result.metadata.get("scan_outcome") == INCONCLUSIVE_SCAN_OUTCOME
and not has_security_findings
and not has_trusted_incomplete_tail
):
if result.metadata.get("scan_outcome") == INCONCLUSIVE_SCAN_OUTCOME and not has_trusted_incomplete_tail:
result.finish(success=False)
else:
result.finish(success=not result.has_errors)
Expand Down
65 changes: 65 additions & 0 deletions tests/scanners/test_joblib_scanner_codecs.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
_validated_numpy_dtype,
np,
)
from modelaudit.scanners.pickle_scanner import PickleScanner
from modelaudit.utils.file.detection import _LZ4_FRAME_MAGIC, validate_file_type_with_formats


Expand Down Expand Up @@ -1376,6 +1377,70 @@ def test_scan_fails_closed_on_protocol1_pickle_in_numpy_wrapper_tail(tmp_path: P
assert "trusted_incomplete_tail" not in result.metadata


def test_scan_fails_closed_on_invalid_numpy_wrapper_with_origin_warning(tmp_path: Path) -> None:
path = tmp_path / "numpy_array_protocol1_origin_warning.joblib"
path.write_bytes(_joblib_numpy_list_payload(resumed_ops=b"\x80\x01K\x01."))

embedded_result = ScanResult("pickle")
embedded_result.add_check(
name="Standalone Pickle Finding",
passed=False,
message="Joblib NumPy wrapper requires origin verification",
severity=IssueSeverity.WARNING,
details={"import_reference": "joblib.numpy_pickle.NumpyArrayWrapper"},
rule_code="NON_ALLOWLISTED_GLOBAL",
)
embedded_result.finish(success=True)

scanner = JoblibScanner()
scanner.pickle_scanner = _StaticEmbeddedPickleScanner(embedded_result)
result = scanner.scan(str(path))

assert result.success is False
assert result.metadata["scan_outcome"] == INCONCLUSIVE_SCAN_OUTCOME
assert result.metadata["analysis_incomplete"] is True
assert "joblib_numpy_array_wrapper_validation_failed" in result.metadata["scan_outcome_reasons"]
assert result.has_warnings is True
assert "trusted_incomplete_tail" not in result.metadata
assert should_cache_scan_result(result.to_dict(include_private_metadata=True)) is False
Comment thread
mldangelo-oai marked this conversation as resolved.
Resolved


def test_scan_fails_closed_when_embedded_pickle_reports_success(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Joblib must fail closed on its own wrapper validation, not on the embedded verdict.

Windows CI reaches this state through the real pickle scanner: the embedded result keeps a
trusted incomplete tail and finishes ``success=True`` while Joblib NumPy-wrapper validation
is still inconclusive. Forcing only the embedded verdict reproduces that lane deterministically
on every platform while leaving payload parsing and metadata composition untouched.
"""
original_finish = PickleScanner._finish_after_wrapper_analysis

def finish_as_successful(self: PickleScanner, result: ScanResult, *, base_success: bool) -> None:
original_finish(self, result, base_success=base_success)
if not result.success:
result.success = True
result._merged_children_success = True

monkeypatch.setattr(PickleScanner, "_finish_after_wrapper_analysis", finish_as_successful)

result = _scan_payload(
tmp_path,
_joblib_numpy_list_payload(resumed_ops=b"\x80\x01K\x01."),
"numpy_array_protocol1_embedded_success.joblib",
)

assert result.success is False
assert result.has_errors is False
assert result.metadata["scan_outcome"] == INCONCLUSIVE_SCAN_OUTCOME
assert result.metadata["analysis_incomplete"] is True
assert "joblib_numpy_array_wrapper_validation_failed" in result.metadata["scan_outcome_reasons"]
assert "trusted_incomplete_tail" not in result.metadata
assert should_cache_scan_result(result.to_dict(include_private_metadata=True)) is False


def test_scan_detects_gzip_compressed_pickle_joblib(tmp_path: Path) -> None:
path = tmp_path / "gzip_protocol4.joblib"
path.write_bytes(gzip.compress(pickle.dumps(_Payload(), protocol=4)))
Expand Down
Loading