diff --git a/CHANGELOG.md b/CHANGELOG.md index 9be4fe097..f974b8ecd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -103,6 +103,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - classify unavailable ZIP traversal, member, manifest-less TorchServe handler, and Keras artifact scan coverage as inconclusive while preserving archive-depth security findings - classify unavailable TAR traversal and member scan coverage as inconclusive while preserving depth-limit security findings - classify unavailable Skops member and schema coverage as inconclusive rather than security findings +- preserve Skops nested Python-member detection for high-risk calls reached through `__getattribute__` - classify bounded, unreadable, or unparseable TorchServe MAR analysis gaps as inconclusive rather than security findings - detect manifest-declared TorchServe extra files and PyTorch ZIP members disguised with executable content - classify incomplete SevenZip coverage as inconclusive and avoid caching temporary extracted members diff --git a/tests/scanners/test_skops_scanner.py b/tests/scanners/test_skops_scanner.py index 3140c2d4f..1a004ccd9 100644 --- a/tests/scanners/test_skops_scanner.py +++ b/tests/scanners/test_skops_scanner.py @@ -715,6 +715,42 @@ def test_oversized_executable_member_is_still_checked_by_nested_scanner(self, tm ) assert determine_exit_code(result) == 1 + def test_python_member_getattribute_high_risk_call_is_reported(self, tmp_path: Path) -> None: + """Skops nested ZIP analysis must catch active code recovered through __getattribute__.""" + skops_file = tmp_path / "getattribute_payload.skops" + source = "import os\nresolve = os.__getattribute__\nrunner = resolve('sys' + 'tem')\nrunner('echo hidden')\n" + with zipfile.ZipFile(skops_file, "w", compression=zipfile.ZIP_DEFLATED) as zf: + zf.writestr("schema.json", '{"version": "1.0"}') + zf.writestr("handler.py", source) + + result = scan_model_directory_or_file(str(skops_file), cache_enabled=False) + + python_issues = [ + issue + for issue in result.issues + if issue.message == "High-risk Python code found in ZIP member handler.py: high-risk calls: os.system" + and issue.details.get("entry") == "handler.py" + and issue.details.get("reason") == "high-risk calls: os.system" + ] + assert len(python_issues) == 1 + assert python_issues[0].severity == IssueSeverity.WARNING + assert determine_exit_code(result) == 1 + + def test_python_member_benign_getattribute_remains_quiet(self, tmp_path: Path) -> None: + """Benign attribute retrieval in Skops Python members should not become a finding.""" + skops_file = tmp_path / "benign_getattribute.skops" + source = "import os\nresolve = os.__getattribute__\ncurrent_dir = resolve('getcwd')\ncurrent_dir()\n" + with zipfile.ZipFile(skops_file, "w", compression=zipfile.ZIP_DEFLATED) as zf: + zf.writestr("schema.json", '{"version": "1.0"}') + zf.writestr("handler.py", source) + + result = scan_model_directory_or_file(str(skops_file), cache_enabled=False) + + assert determine_exit_code(result) == 0 + assert not any( + issue.message.startswith("High-risk Python code found in ZIP member handler.py") for issue in result.issues + ) + @pytest.mark.parametrize( ("exception_type", "message"), [