Skip to content

fix: reduce Keras ZIP custom-object false positives#716

Merged
mldangelo merged 3 commits intomainfrom
fix/keras-zip-safe-object-detection
Mar 18, 2026
Merged

fix: reduce Keras ZIP custom-object false positives#716
mldangelo merged 3 commits intomainfrom
fix/keras-zip-safe-object-detection

Conversation

@yash2998chhabria
Copy link
Contributor

@yash2998chhabria yash2998chhabria commented Mar 16, 2026

Summary

  • stop flagging safe built-in or allowlisted serialized Keras objects as custom layer/object findings when they come from trusted module roots
  • preserve warnings for genuinely custom Keras layers, metrics, and losses that still require external code review
  • add regression coverage for built-in registered objects and allowlisted-module serialized ops

Validation

  • /Users/yashchhabria/projects/modelauditing/modelaudit/.venv/bin/ruff format modelaudit/ tests/
  • /Users/yashchhabria/projects/modelauditing/modelaudit/.venv/bin/ruff check --fix modelaudit/ tests/
  • /Users/yashchhabria/projects/modelauditing/modelaudit/.venv/bin/mypy modelaudit/
  • /Users/yashchhabria/projects/modelauditing/modelaudit/.venv/bin/pytest -n auto -m "not slow and not integration" --maxfail=1
  • 10-model Hugging Face rerun in the worktree: removed false positives for safe Add and NotEqual serialized objects while preserving remaining custom-layer and custom-metric/loss findings

Summary by CodeRabbit

  • Bug Fixes

    • Eliminated false positives when scanning Keras ZIP models by adding allowlist-based safety checks and expanding known-safe layer names (e.g., Add, NotEqual).
  • Tests

    • Added tests to confirm built-in layers and allowlisted modules do not trigger false positives while still flagging unknown custom layers.

Avoid flagging safe built-in or allowlisted serialized layer objects
as custom objects while preserving warnings for genuinely custom
Keras layers, metrics, and losses.

Co-Authored-By: Codex <noreply@openai.com>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 16, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: c122fa2b-23a6-45cc-875f-c79994c7441c

📥 Commits

Reviewing files that changed from the base of the PR and between 3242abb and 1381e2f.

📒 Files selected for processing (1)
  • CHANGELOG.md

Walkthrough

Adds allowlist-based safety checks and known-safe layer recognition to the Keras ZIP scanner, updates KNOWN_SAFE_KERAS_LAYER_CLASSES with "NotEqual", updates the changelog, and adds tests to prevent false positives for built-in and allowlisted serialized objects.

Changes

Cohort / File(s) Summary
Changelog
CHANGELOG.md
Added Unreleased entry noting elimination of Keras ZIP false positives for safe built-in and allowlisted serialized objects (e.g., Add, NotEqual).
Scanner logic
modelaudit/scanners/keras_zip_scanner.py
Introduced helper methods _is_allowlisted_keras_module, _layer_uses_allowlisted_module, _is_known_safe_allowlisted_registered_object, _is_known_safe_serialized_layer, and _should_flag_registered_object. Refactored _scan_model_config to use these centralized checks when scanning layers and registered objects.
Detectors
modelaudit/detectors/suspicious_symbols.py
Expanded KNOWN_SAFE_KERAS_LAYER_CLASSES to include NotEqual.
Tests
tests/scanners/test_keras_zip_scanner.py
Added three tests to ensure built-in and allowlisted serialized layers (e.g., Add, NotEqual) do not false-positive and that unknown layers in allowlisted modules remain flagged.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 I hopped through configs, ears tuned to the trace,
Added guards and allowlists to tidy the place.
Add and NotEqual now skip the alarm,
Unknown layers still ring—no loss, only charm. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: reducing false positives when detecting custom objects in Keras ZIP files by adding allowlist-based safety checks for built-in and allowlisted serialized objects.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/keras-zip-safe-object-detection
📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

@yash2998chhabria
Copy link
Contributor Author

Testing Metadata Update

  • Timestamp: 2026-03-16T12:11:49.545724-07:00
  • Validation method: one external worktree per changed scanner, public Hugging Face models relevant to each scanner, target 10 models per scanner where available
  • Parallelism: 5 workers, chosen after resource monitoring on this machine (14 CPU, 24 GiB RAM, tight disk headroom)
  • Broad changed-scanner sweep status: 39/39 scanners completed
  • Pending broad-sweep scanner: none
  • Note: the broad table below is the baseline run on main before PR-specific fixes; PR-focused reruns are listed separately

Branch Validation

  • ruff format modelaudit/ tests/
  • ruff check --fix modelaudit/ tests/
  • mypy modelaudit/
  • pytest -n auto -m "not slow and not integration" --maxfail=1
Broad Hugging Face Sweep (baseline on main)
scanner discovered flagged clean
catboost 10 0 10
cntk 0 0 0
compressed 0 0 0
coreml 10 0 10
executorch 10 10 0
flax_msgpack 10 0 10
gguf 10 0 10
jax_checkpoint 2 0 2
joblib 10 3 7
keras_h5 10 1 9
keras_zip 10 7 3
lightgbm 10 0 10
llamafile 5 0 5
manifest 10 0 10
mxnet 0 0 0
numpy 10 1 9
oci_layer 0 0 0
onnx 10 0 10
openvino 10 0 10
paddle 3 0 3
pickle 10 10 0
pmml 0 0 0
pytorch_binary 10 0 10
pytorch_zip 10 4 6
r_serialized 1 1 0
rknn 10 1 9
safetensors 10 0 10
skops 4 0 4
tar 1 1 0
tensorrt 10 10 0
text 10 0 10
tf_metagraph 7 3 4
tf_savedmodel 10 9 1
tflite 10 1 9
torch7 0 0 0
torchserve_mar 0 0 0
weight_distribution 10 0 10
xgboost 10 0 10
zip 8 0 8

Focused Rerun For This PR

  • Scanner: keras_zip
  • Baseline on main: discovered=10, flagged=7, clean=3
  • Post-fix rerun in PR worktree: discovered=10, flagged=6, clean=4
  • Regression outcome: false positives for safe built-in / allowlisted serialized objects such as Add and NotEqual were removed; remaining hits in the rerun are custom-layer, custom-metric/loss, or non-ZIP .keras cases that still warrant review

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@modelaudit/scanners/keras_zip_scanner.py`:
- Around line 128-145: The current early return in
_should_flag_registered_object when
is_known_safe_keras_layer_class(registered_name) is True bypasses module-root
trust checks; change the logic so known-safe registered_name does not
short-circuit detection — instead, continue to evaluate module trust and
serialized-safety: call _layer_uses_allowlisted_module and
_is_known_safe_allowlisted_registered_object (and still keep the
_is_known_safe_serialized_layer short-circuit) so that only layers both
serialized-safe or coming from an allowlisted module/registered-object are
treated as safe; update the branch in _should_flag_registered_object (and the
equality branch comparing registered_name and layer_class) to require module
allowlist checks even for known-safe registered_name before returning False.

In `@tests/scanners/test_keras_zip_scanner.py`:
- Around line 540-623: Add a regression test to ensure trusted-looking names
from non-allowlisted modules still trigger detection: in
tests/scanners/test_keras_zip_scanner.py create a new test (or extend an
existing one) using KerasZipScanner that builds a model config where a layer has
class_name="Add", registered_name="Add", but module="my_custom_pkg.ops"
(non-allowlisted), call scanner.scan on create_configured_keras_zip(tmp_path,
config, file_name="untrusted_add.keras") and assert that result.checks contains
a check with name "Custom Object Detection" (and/or "Custom Layer Class
Detection") to lock in trust-root enforcement.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3c00ef74-70af-4b9c-87f8-5b2d50e3676d

📥 Commits

Reviewing files that changed from the base of the PR and between 26e6b97 and 3242abb.

📒 Files selected for processing (3)
  • modelaudit/detectors/suspicious_symbols.py
  • modelaudit/scanners/keras_zip_scanner.py
  • tests/scanners/test_keras_zip_scanner.py

Comment on lines +128 to +145
def _should_flag_registered_object(self, layer: dict[str, Any]) -> bool:
registered_name = layer.get("registered_name")
if not isinstance(registered_name, str) or not registered_name.strip():
return False

if is_known_safe_keras_layer_class(registered_name):
return False

layer_class = layer.get("class_name")
if isinstance(layer_class, str) and registered_name.strip() == layer_class.strip():
if self._is_known_safe_serialized_layer(layer):
return False
return not (
self._layer_uses_allowlisted_module(layer)
and self._is_known_safe_allowlisted_registered_object(layer_class)
)

return True
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Known-safe registered_name currently bypasses trust-root validation.

At Line 133, _should_flag_registered_object exits early for any known-safe registered_name before module-root trust is considered. This allows non-allowlisted module metadata to avoid Custom Object Detection at Line 425.

🔧 Suggested fix
 def _should_flag_registered_object(self, layer: dict[str, Any]) -> bool:
     registered_name = layer.get("registered_name")
     if not isinstance(registered_name, str) or not registered_name.strip():
         return False

-    if is_known_safe_keras_layer_class(registered_name):
-        return False
+    if is_known_safe_keras_layer_class(registered_name):
+        module_values: list[str] = []
+        layer_config = layer.get("config", {})
+        if isinstance(layer_config, dict):
+            for key in ("module", "fn_module"):
+                value = layer_config.get(key)
+                if isinstance(value, str) and value.strip():
+                    module_values.append(value.strip())
+        for key in ("module", "fn_module"):
+            value = layer.get(key)
+            if isinstance(value, str) and value.strip():
+                module_values.append(value.strip())
+
+        # Suppress only when module metadata is absent or fully allowlisted.
+        if not module_values or all(self._is_allowlisted_keras_module(v) for v in module_values):
+            return False
+        return True

Based on learnings: Preserve or strengthen security detections; test both benign and malicious samples when adding scanner/feature changes.

Also applies to: 425-425

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@modelaudit/scanners/keras_zip_scanner.py` around lines 128 - 145, The current
early return in _should_flag_registered_object when
is_known_safe_keras_layer_class(registered_name) is True bypasses module-root
trust checks; change the logic so known-safe registered_name does not
short-circuit detection — instead, continue to evaluate module trust and
serialized-safety: call _layer_uses_allowlisted_module and
_is_known_safe_allowlisted_registered_object (and still keep the
_is_known_safe_serialized_layer short-circuit) so that only layers both
serialized-safe or coming from an allowlisted module/registered-object are
treated as safe; update the branch in _should_flag_registered_object (and the
equality branch comparing registered_name and layer_class) to require module
allowlist checks even for known-safe registered_name before returning False.

Comment on lines +540 to +623
def test_registered_builtin_layer_does_not_false_positive(self, tmp_path: Path) -> None:
"""Built-in layers with registered_name metadata should remain clean."""
scanner = KerasZipScanner()
config = {
"class_name": "Functional",
"config": {
"layers": [
{
"class_name": "InputLayer",
"name": "input_1",
"config": {"batch_shape": [None, 4]},
},
{
"class_name": "Add",
"name": "add_1",
"module": "keras.src.ops.numpy",
"registered_name": "Add",
"config": {},
},
]
},
}

result = scanner.scan(str(create_configured_keras_zip(tmp_path, config, file_name="builtin_registered.keras")))

assert all(check.name != "Custom Layer Class Detection" for check in result.checks)
assert all(check.name != "Custom Object Detection" for check in result.checks)

def test_allowlisted_module_layer_does_not_false_positive(self, tmp_path: Path) -> None:
"""Layers from allowlisted Keras modules should not be treated as custom objects."""
scanner = KerasZipScanner()
config = {
"class_name": "Functional",
"config": {
"layers": [
{
"class_name": "InputLayer",
"name": "input_1",
"config": {"batch_shape": [None, 4]},
},
{
"class_name": "NotEqual",
"name": "not_equal",
"module": "keras.src.ops.numpy",
"registered_name": "NotEqual",
"config": {},
},
]
},
}

result = scanner.scan(str(create_configured_keras_zip(tmp_path, config, file_name="allowlisted_module.keras")))

assert all(check.name != "Custom Layer Class Detection" for check in result.checks)
assert all(check.name != "Custom Object Detection" for check in result.checks)

def test_allowlisted_module_does_not_suppress_unknown_custom_layer(self, tmp_path: Path) -> None:
"""Unknown classes must still be flagged even if module metadata looks Keras-owned."""
scanner = KerasZipScanner()
config = {
"class_name": "Functional",
"config": {
"layers": [
{
"class_name": "InputLayer",
"name": "input_1",
"config": {"batch_shape": [None, 4]},
},
{
"class_name": "EvilLayer",
"name": "evil_layer",
"module": "keras.src.ops.numpy",
"registered_name": "EvilLayer",
"config": {},
},
]
},
}

result = scanner.scan(str(create_configured_keras_zip(tmp_path, config, file_name="evil_allowlisted.keras")))

assert any(check.name == "Custom Layer Class Detection" for check in result.checks)
assert any(check.name == "Custom Object Detection" for check in result.checks)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add one regression for safe class names coming from non-allowlisted modules.

These tests are good, but please add a case like class_name="Add", registered_name="Add", module="my_custom_pkg.ops" and assert Custom Object Detection is raised. That locks in trust-root enforcement.

Based on learnings: Preserve or strengthen security detections; test both benign and malicious samples when adding scanner/feature changes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/scanners/test_keras_zip_scanner.py` around lines 540 - 623, Add a
regression test to ensure trusted-looking names from non-allowlisted modules
still trigger detection: in tests/scanners/test_keras_zip_scanner.py create a
new test (or extend an existing one) using KerasZipScanner that builds a model
config where a layer has class_name="Add", registered_name="Add", but
module="my_custom_pkg.ops" (non-allowlisted), call scanner.scan on
create_configured_keras_zip(tmp_path, config, file_name="untrusted_add.keras")
and assert that result.checks contains a check with name "Custom Object
Detection" (and/or "Custom Layer Class Detection") to lock in trust-root
enforcement.

@mldangelo mldangelo merged commit 165b238 into main Mar 18, 2026
27 checks passed
@mldangelo mldangelo deleted the fix/keras-zip-safe-object-detection branch March 18, 2026 15:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants