diff --git a/CHANGELOG.md b/CHANGELOG.md index a4c2a75cc..e5504afd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Bug Fixes +- flag Paddle code patterns as warnings instead of failing benign scans - route corrupt CatBoost scans to fail closed outcomes - mark incomplete MXNet scans inconclusive instead of clean - harden manifest parse boundaries around malformed metadata diff --git a/modelaudit/scanners/paddle_scanner.py b/modelaudit/scanners/paddle_scanner.py index ed3d8f62c..6a19749ac 100644 --- a/modelaudit/scanners/paddle_scanner.py +++ b/modelaudit/scanners/paddle_scanner.py @@ -117,7 +117,7 @@ def _check_chunk( name="Binary Pattern Detection", passed=False, message=f"Suspicious binary pattern found: {pattern.decode('ascii', 'ignore')}", - severity=IssueSeverity.INFO, + severity=IssueSeverity.WARNING, location=f"{path} (offset: {offset + pos})", details={"pattern": pattern.decode("ascii", "ignore"), "offset": offset + pos}, rule_code="S902", @@ -137,7 +137,7 @@ def _check_chunk( name="String Pattern Detection", passed=False, message=f"Suspicious string pattern found: {regex}", - severity=IssueSeverity.INFO, + severity=IssueSeverity.WARNING, location=path, details={"pattern": regex}, rule_code="S902", diff --git a/tests/scanners/test_paddle_scanner.py b/tests/scanners/test_paddle_scanner.py index 13ba7ce1d..7f7ede7a4 100644 --- a/tests/scanners/test_paddle_scanner.py +++ b/tests/scanners/test_paddle_scanner.py @@ -2,6 +2,7 @@ from pathlib import Path from unittest.mock import patch +from modelaudit.core import determine_exit_code, scan_model_directory_or_file from modelaudit.scanners.base import IssueSeverity from modelaudit.scanners.paddle_scanner import PaddleScanner from modelaudit.utils.file.detection import validate_file_type @@ -28,7 +29,25 @@ def test_paddle_scanner_detects_suspicious_pattern(tmp_path: Path) -> None: with patch("modelaudit.scanners.paddle_scanner.HAS_PADDLE", True): scanner = PaddleScanner() result = scanner.scan(str(path)) - assert any("suspicious" in i.message.lower() for i in result.issues) + suspicious_issues = [i for i in result.issues if "suspicious" in i.message.lower()] + + assert suspicious_issues + assert all(issue.severity == IssueSeverity.WARNING for issue in suspicious_issues) + + +def test_paddle_suspicious_pdmodel_aggregate_exit_code_is_security_finding(tmp_path: Path) -> None: + """Suspicious .pdmodel patterns should be warning-level security findings.""" + path = tmp_path / "model.pdmodel" + path.write_bytes(b"os.system('ls')") + + with patch("modelaudit.scanners.paddle_scanner.HAS_PADDLE", True): + result = scan_model_directory_or_file(str(path), cache_scan_results=False) + + warning_issues = [issue for issue in result.issues if issue.severity == IssueSeverity.WARNING] + assert result.success is True + assert result.has_errors is False + assert determine_exit_code(result) == 1 + assert warning_issues def test_paddle_scanner_missing_dependency(tmp_path: Path) -> None: @@ -104,6 +123,22 @@ def test_pdiparams_real_threats_still_detected(tmp_path: Path) -> None: assert any("import os" in p for p in patterns_found), "import os should be detected" assert any("eval(" in p for p in patterns_found), "eval( should be detected" assert any("os.system" in p for p in patterns_found), "os.system should be detected" + assert all(i.severity == IssueSeverity.WARNING for i in result.issues) + + +def test_paddle_suspicious_pdiparams_aggregate_exit_code_is_security_finding(tmp_path: Path) -> None: + """Suspicious .pdiparams patterns should be warning-level security findings.""" + path = tmp_path / "bad_weights.pdiparams" + path.write_bytes(b"padding " + b"import os" + b" eval(payload) " + b"os.system('rm -rf /')") + + with patch("modelaudit.scanners.paddle_scanner.HAS_PADDLE", True): + result = scan_model_directory_or_file(str(path), cache_scan_results=False) + + warning_issues = [issue for issue in result.issues if issue.severity == IssueSeverity.WARNING] + assert result.success is True + assert result.has_errors is False + assert determine_exit_code(result) == 1 + assert warning_issues def test_pdmodel_hex_escape_still_flagged(tmp_path: Path) -> None: