Instructions to use pragnyanramtha/keras-native-nonlayer-lambda-modelscan-bypass-poc with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Keras
How to use pragnyanramtha/keras-native-nonlayer-lambda-modelscan-bypass-poc with Keras:
# Available backend options are: "jax", "torch", "tensorflow". import os os.environ["KERAS_BACKEND"] = "jax" import keras model = keras.saving.load_model("hf://pragnyanramtha/keras-native-nonlayer-lambda-modelscan-bypass-poc") - Notebooks
- Google Colab
- Kaggle
Upload folder using huggingface_hub
Browse files- README.md +89 -0
- requirements.txt +5 -0
- results/activation_modelscan_results.json +1 -0
- results/activation_runtime_results.txt +13 -0
- results/ops_map_modelscan_results.json +1 -0
- results/ops_map_runtime_results.json +39 -0
- results/vectorized_map_modelscan_results.json +1 -0
- results/vectorized_map_runtime_results.json +39 -0
- variants/activation/activation_lambda_manual_config.keras +0 -0
- variants/activation/activation_lambda_manual_config.modelscan.json +1 -0
- variants/ops_map/ops_map_inbound_lambda.keras +0 -0
- variants/ops_map/ops_map_inbound_lambda.modelscan.json +1 -0
- variants/vectorized_map/vectorized_map_lambda.keras +0 -0
- variants/vectorized_map/vectorized_map_lambda.modelscan.json +1 -0
- verify_all.py +167 -0
README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
library_name: keras
|
| 3 |
+
tags:
|
| 4 |
+
- security
|
| 5 |
+
- keras
|
| 6 |
+
- modelscan
|
| 7 |
+
- proof-of-concept
|
| 8 |
+
license: apache-2.0
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
# Keras Native Non-Layer Lambda ModelScan Bypass PoC
|
| 12 |
+
|
| 13 |
+
Benign security proof-of-concept for a ModelScan Keras Native scanner/runtime mismatch.
|
| 14 |
+
|
| 15 |
+
## Summary
|
| 16 |
+
|
| 17 |
+
ModelScan 0.8.8 flags top-level Keras `Lambda` layers, but it misses marshalled Python `__lambda__` bytecode stored in other `.keras` native `config.json` fields. Keras 3.14.1 blocks these artifacts by default with `safe_mode=True`. If a workflow opts into unsafe deserialization with `safe_mode=False`, Keras consumes the artifact-carried lambda and executes benign marker code.
|
| 18 |
+
|
| 19 |
+
This repository contains three variants of the same scanner weakness:
|
| 20 |
+
|
| 21 |
+
1. `Dense.activation` contains `class_name: "__lambda__"`.
|
| 22 |
+
2. `keras.ops.map` contains an inbound-node lambda argument.
|
| 23 |
+
3. `keras.ops.vectorized_map` contains a lambda in the operation config.
|
| 24 |
+
|
| 25 |
+
ModelScan reports zero issues for all three `.keras` archives.
|
| 26 |
+
|
| 27 |
+
## Severity
|
| 28 |
+
|
| 29 |
+
Medium, CVSS 5.3.
|
| 30 |
+
|
| 31 |
+
Rationale: the impact is a scanner false negative for artifact-carried Python bytecode that can execute during unsafe Keras deserialization. The severity is capped because this is not a default Keras `safe_mode=True` bypass.
|
| 32 |
+
|
| 33 |
+
## Tested Versions
|
| 34 |
+
|
| 35 |
+
- `keras==3.14.1`
|
| 36 |
+
- `tensorflow==2.20.0`
|
| 37 |
+
- `modelscan==0.8.8`
|
| 38 |
+
- `h5py==3.16.0`
|
| 39 |
+
- Python 3.12.3
|
| 40 |
+
|
| 41 |
+
## Files
|
| 42 |
+
|
| 43 |
+
- `variants/activation/activation_lambda_manual_config.keras`
|
| 44 |
+
- `variants/ops_map/ops_map_inbound_lambda.keras`
|
| 45 |
+
- `variants/vectorized_map/vectorized_map_lambda.keras`
|
| 46 |
+
- `verify_all.py`: verifies safe-mode blocking, unsafe marker execution, lambda locations, and ModelScan output.
|
| 47 |
+
- `results/`: captured local validation outputs.
|
| 48 |
+
- `requirements.txt`: tested dependency versions.
|
| 49 |
+
|
| 50 |
+
## Reproduce
|
| 51 |
+
|
| 52 |
+
```bash
|
| 53 |
+
python -m venv .venv
|
| 54 |
+
. .venv/bin/activate
|
| 55 |
+
pip install -r requirements.txt
|
| 56 |
+
|
| 57 |
+
python verify_all.py
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
Expected result:
|
| 61 |
+
|
| 62 |
+
- `safe_mode_true` starts with `blocked:` for all variants.
|
| 63 |
+
- `marker_after_safe_mode_true` is `false` for all variants.
|
| 64 |
+
- unsafe deserialization creates the expected benign marker for all variants.
|
| 65 |
+
- `modelscan_total_issues` is `0` for all variants.
|
| 66 |
+
- `all_passed` is `true`.
|
| 67 |
+
|
| 68 |
+
## Artifact Hashes
|
| 69 |
+
|
| 70 |
+
```text
|
| 71 |
+
f43f6dd253cb5f4683a2903ea5b6d7ae744573c7f4fbdb82d584974709ee3be8 variants/activation/activation_lambda_manual_config.keras
|
| 72 |
+
2a900cb614d5e0b6eef831e0bd4caafc6503daf6d33963ec6a64f7fa447d6cdc variants/ops_map/ops_map_inbound_lambda.keras
|
| 73 |
+
536334212fdea8cf9a19cf71d0c4decd74442116eb31ee39aab0c9d28b13757e variants/vectorized_map/vectorized_map_lambda.keras
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
## Impact
|
| 77 |
+
|
| 78 |
+
A registry or deployment workflow can scan a `.keras` artifact with ModelScan, receive a clean result, and later execute artifact-carried Python bytecode if it loads the model with unsafe Keras deserialization enabled. The PoC keeps the effect benign by writing local marker files only.
|
| 79 |
+
|
| 80 |
+
## Limitations
|
| 81 |
+
|
| 82 |
+
- Not a Keras default safe-mode bypass.
|
| 83 |
+
- Requires `safe_mode=False` or equivalent unsafe deserialization opt-in.
|
| 84 |
+
- Does not execute shell commands, access credentials, persist, or perform network activity.
|
| 85 |
+
- Related public work exists for top-level/wrapped Lambda misses and HDF5 activation-field bypasses; this PoC demonstrates current Keras native `.keras` non-layer `__lambda__` fields.
|
| 86 |
+
|
| 87 |
+
## Suggested Mitigation
|
| 88 |
+
|
| 89 |
+
ModelScan's Keras Native scanner should recursively inspect `config.json` for `class_name: "__lambda__"` and other unsafe deserialization markers, not only top-level layers whose `class_name` is `Lambda`.
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
keras==3.14.1
|
| 2 |
+
tensorflow==2.20.0
|
| 3 |
+
modelscan==0.8.8
|
| 4 |
+
h5py==3.16.0
|
| 5 |
+
numpy==2.3.4
|
results/activation_modelscan_results.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"summary": {"total_issues_by_severity": {"LOW": 0, "MEDIUM": 0, "HIGH": 0, "CRITICAL": 0}, "total_issues": 0, "input_path": "activation_lambda_manual_config.keras", "absolute_path": "/workspace/keras_native/hf_keras_native_activation_lambda_poc", "modelscan_version": "0.8.8", "timestamp": "2026-05-12T13:18:10.152015", "scanned": {"total_scanned": 1, "scanned_files": ["activation_lambda_manual_config.keras"]}, "skipped": {"total_skipped": 3, "skipped_files": [{"category": "SCAN_NOT_SUPPORTED", "description": "Model Scan did not scan file", "source": "activation_lambda_manual_config.keras:metadata.json"}, {"category": "SCAN_NOT_SUPPORTED", "description": "Model Scan did not scan file", "source": "activation_lambda_manual_config.keras:config.json"}, {"category": "MODEL_CONFIG", "description": "Model Config not found", "source": "activation_lambda_manual_config.keras:model.weights.h5"}]}}, "issues": [], "errors": []}
|
results/activation_runtime_results.txt
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
keras_version=3.14.1
|
| 2 |
+
artifact=/workspace/keras_native/hf_keras_native_activation_lambda_poc/activation_lambda_manual_config.keras
|
| 3 |
+
working_directory=/workspace/keras_native/hf_keras_native_activation_lambda_poc
|
| 4 |
+
config_summary={"activation_class": "__lambda__", "activation_has_marshaled_value": false, "activation_keys": ["class_name", "config"], "has_top_level_lambda_layer": false, "layer_class": "Dense"}
|
| 5 |
+
safe_mode_true=blocked:TypeError:<class 'keras.src.models.sequential.Sequential'> could not be deserialized properly. Please ensure that components that are Python object instances (layers, models, etc.) returned by `get_config()` are explicitly deserialized in the model's `from_config()` method.
|
| 6 |
+
marker_after_safe_mode_true=False
|
| 7 |
+
marker_after_safe_mode_false_load=False
|
| 8 |
+
marker_after_safe_mode_false_infer=True
|
| 9 |
+
marker_content=KERAS_NATIVE_ACTIVATION_LAMBDA_MARKER
|
| 10 |
+
modelscan_returncode=0
|
| 11 |
+
modelscan_summary={}
|
| 12 |
+
modelscan_issues=[]
|
| 13 |
+
modelscan_errors=[]
|
results/ops_map_modelscan_results.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"summary": {"total_issues_by_severity": {"LOW": 0, "MEDIUM": 0, "HIGH": 0, "CRITICAL": 0}, "total_issues": 0, "input_path": "/workspace/keras_native/hf_keras_native_ops_map_poc/ops_map_inbound_lambda.keras", "absolute_path": "/workspace/keras_native/hf_keras_native_ops_map_poc", "modelscan_version": "0.8.8", "timestamp": "2026-05-12T14:01:04.593668", "scanned": {"total_scanned": 1, "scanned_files": ["ops_map_inbound_lambda.keras"]}, "skipped": {"total_skipped": 3, "skipped_files": [{"category": "SCAN_NOT_SUPPORTED", "description": "Model Scan did not scan file", "source": "ops_map_inbound_lambda.keras:metadata.json"}, {"category": "SCAN_NOT_SUPPORTED", "description": "Model Scan did not scan file", "source": "ops_map_inbound_lambda.keras:config.json"}, {"category": "MODEL_CONFIG", "description": "Model Config not found", "source": "ops_map_inbound_lambda.keras:model.weights.h5"}]}}, "issues": [], "errors": []}
|
results/ops_map_runtime_results.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"artifact": "/workspace/keras_native/hf_keras_native_ops_map_poc/ops_map_inbound_lambda.keras",
|
| 3 |
+
"keras_version": "3.14.1",
|
| 4 |
+
"lambda_paths": [
|
| 5 |
+
"$.config.layers[1].inbound_nodes[0].args[0]"
|
| 6 |
+
],
|
| 7 |
+
"marker_after_safe_mode_false_load": true,
|
| 8 |
+
"marker_after_safe_mode_false_load_content": "OPS_MAP_MARKER",
|
| 9 |
+
"marker_after_safe_mode_true": false,
|
| 10 |
+
"marker_after_unsafe_inference": true,
|
| 11 |
+
"modelscan_issues": [],
|
| 12 |
+
"modelscan_returncode": 0,
|
| 13 |
+
"modelscan_skipped": {
|
| 14 |
+
"skipped_files": [
|
| 15 |
+
{
|
| 16 |
+
"category": "SCAN_NOT_SUPPORTED",
|
| 17 |
+
"description": "Model Scan did not scan file",
|
| 18 |
+
"source": "ops_map_inbound_lambda.keras:metadata.json"
|
| 19 |
+
},
|
| 20 |
+
{
|
| 21 |
+
"category": "SCAN_NOT_SUPPORTED",
|
| 22 |
+
"description": "Model Scan did not scan file",
|
| 23 |
+
"source": "ops_map_inbound_lambda.keras:config.json"
|
| 24 |
+
},
|
| 25 |
+
{
|
| 26 |
+
"category": "MODEL_CONFIG",
|
| 27 |
+
"description": "Model Config not found",
|
| 28 |
+
"source": "ops_map_inbound_lambda.keras:model.weights.h5"
|
| 29 |
+
}
|
| 30 |
+
],
|
| 31 |
+
"total_skipped": 3
|
| 32 |
+
},
|
| 33 |
+
"modelscan_total_issues": 0,
|
| 34 |
+
"modelscan_version": "0.8.8",
|
| 35 |
+
"safe_mode_false_load": "loaded",
|
| 36 |
+
"safe_mode_true": "blocked:ValueError:Requested the deserialization of a Python lambda. This carries a potential risk of arbitrary code execution and thus it is disallowed by default. If you trust the source of the artifact, you can override this error by passing `safe_mode=False` to the loading function, or calling `keras.config.enable_unsafe_deserialization().",
|
| 37 |
+
"sha256": "2a900cb614d5e0b6eef831e0bd4caafc6503daf6d33963ec6a64f7fa447d6cdc",
|
| 38 |
+
"unsafe_inference": "ok"
|
| 39 |
+
}
|
results/vectorized_map_modelscan_results.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"summary": {"total_issues_by_severity": {"LOW": 0, "MEDIUM": 0, "HIGH": 0, "CRITICAL": 0}, "total_issues": 0, "input_path": "/workspace/keras_native/hf_keras_native_vectorized_map_poc/vectorized_map_lambda.keras", "absolute_path": "/workspace/keras_native/hf_keras_native_vectorized_map_poc", "modelscan_version": "0.8.8", "timestamp": "2026-05-12T14:01:42.382025", "scanned": {"total_scanned": 1, "scanned_files": ["vectorized_map_lambda.keras"]}, "skipped": {"total_skipped": 3, "skipped_files": [{"category": "SCAN_NOT_SUPPORTED", "description": "Model Scan did not scan file", "source": "vectorized_map_lambda.keras:metadata.json"}, {"category": "SCAN_NOT_SUPPORTED", "description": "Model Scan did not scan file", "source": "vectorized_map_lambda.keras:config.json"}, {"category": "MODEL_CONFIG", "description": "Model Config not found", "source": "vectorized_map_lambda.keras:model.weights.h5"}]}}, "issues": [], "errors": []}
|
results/vectorized_map_runtime_results.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"artifact": "/workspace/keras_native/hf_keras_native_vectorized_map_poc/vectorized_map_lambda.keras",
|
| 3 |
+
"keras_version": "3.14.1",
|
| 4 |
+
"lambda_paths": [
|
| 5 |
+
"$.config.layers[1].config.function"
|
| 6 |
+
],
|
| 7 |
+
"marker_after_safe_mode_false_load": true,
|
| 8 |
+
"marker_after_safe_mode_false_load_content": "VECTORIZED_MAP_MARKER",
|
| 9 |
+
"marker_after_safe_mode_true": false,
|
| 10 |
+
"marker_after_unsafe_inference": true,
|
| 11 |
+
"modelscan_issues": [],
|
| 12 |
+
"modelscan_returncode": 0,
|
| 13 |
+
"modelscan_skipped": {
|
| 14 |
+
"skipped_files": [
|
| 15 |
+
{
|
| 16 |
+
"category": "SCAN_NOT_SUPPORTED",
|
| 17 |
+
"description": "Model Scan did not scan file",
|
| 18 |
+
"source": "vectorized_map_lambda.keras:metadata.json"
|
| 19 |
+
},
|
| 20 |
+
{
|
| 21 |
+
"category": "SCAN_NOT_SUPPORTED",
|
| 22 |
+
"description": "Model Scan did not scan file",
|
| 23 |
+
"source": "vectorized_map_lambda.keras:config.json"
|
| 24 |
+
},
|
| 25 |
+
{
|
| 26 |
+
"category": "MODEL_CONFIG",
|
| 27 |
+
"description": "Model Config not found",
|
| 28 |
+
"source": "vectorized_map_lambda.keras:model.weights.h5"
|
| 29 |
+
}
|
| 30 |
+
],
|
| 31 |
+
"total_skipped": 3
|
| 32 |
+
},
|
| 33 |
+
"modelscan_total_issues": 0,
|
| 34 |
+
"modelscan_version": "0.8.8",
|
| 35 |
+
"safe_mode_false_load": "loaded",
|
| 36 |
+
"safe_mode_true": "blocked:ValueError:Requested the deserialization of a Python lambda. This carries a potential risk of arbitrary code execution and thus it is disallowed by default. If you trust the source of the artifact, you can override this error by passing `safe_mode=False` to the loading function, or calling `keras.config.enable_unsafe_deserialization().",
|
| 37 |
+
"sha256": "536334212fdea8cf9a19cf71d0c4decd74442116eb31ee39aab0c9d28b13757e",
|
| 38 |
+
"unsafe_inference": "ok"
|
| 39 |
+
}
|
variants/activation/activation_lambda_manual_config.keras
ADDED
|
Binary file (1.72 kB). View file
|
|
|
variants/activation/activation_lambda_manual_config.modelscan.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"summary": {"total_issues_by_severity": {"LOW": 0, "MEDIUM": 0, "HIGH": 0, "CRITICAL": 0}, "total_issues": 0, "input_path": "/workspace/keras_native/hf_keras_native_nonlayer_lambda_poc/variants/activation/activation_lambda_manual_config.keras", "absolute_path": "/workspace/keras_native/hf_keras_native_nonlayer_lambda_poc/variants/activation", "modelscan_version": "0.8.8", "timestamp": "2026-05-12T15:24:59.629625", "scanned": {"total_scanned": 1, "scanned_files": ["activation_lambda_manual_config.keras"]}, "skipped": {"total_skipped": 3, "skipped_files": [{"category": "SCAN_NOT_SUPPORTED", "description": "Model Scan did not scan file", "source": "activation_lambda_manual_config.keras:metadata.json"}, {"category": "SCAN_NOT_SUPPORTED", "description": "Model Scan did not scan file", "source": "activation_lambda_manual_config.keras:config.json"}, {"category": "MODEL_CONFIG", "description": "Model Config not found", "source": "activation_lambda_manual_config.keras:model.weights.h5"}]}}, "issues": [], "errors": []}
|
variants/ops_map/ops_map_inbound_lambda.keras
ADDED
|
Binary file (10.8 kB). View file
|
|
|
variants/ops_map/ops_map_inbound_lambda.modelscan.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"summary": {"total_issues_by_severity": {"LOW": 0, "MEDIUM": 0, "HIGH": 0, "CRITICAL": 0}, "total_issues": 0, "input_path": "/workspace/keras_native/hf_keras_native_nonlayer_lambda_poc/variants/ops_map/ops_map_inbound_lambda.keras", "absolute_path": "/workspace/keras_native/hf_keras_native_nonlayer_lambda_poc/variants/ops_map", "modelscan_version": "0.8.8", "timestamp": "2026-05-12T15:25:16.819075", "scanned": {"total_scanned": 1, "scanned_files": ["ops_map_inbound_lambda.keras"]}, "skipped": {"total_skipped": 3, "skipped_files": [{"category": "SCAN_NOT_SUPPORTED", "description": "Model Scan did not scan file", "source": "ops_map_inbound_lambda.keras:metadata.json"}, {"category": "SCAN_NOT_SUPPORTED", "description": "Model Scan did not scan file", "source": "ops_map_inbound_lambda.keras:config.json"}, {"category": "MODEL_CONFIG", "description": "Model Config not found", "source": "ops_map_inbound_lambda.keras:model.weights.h5"}]}}, "issues": [], "errors": []}
|
variants/vectorized_map/vectorized_map_lambda.keras
ADDED
|
Binary file (10.9 kB). View file
|
|
|
variants/vectorized_map/vectorized_map_lambda.modelscan.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"summary": {"total_issues_by_severity": {"LOW": 0, "MEDIUM": 0, "HIGH": 0, "CRITICAL": 0}, "total_issues": 0, "input_path": "/workspace/keras_native/hf_keras_native_nonlayer_lambda_poc/variants/vectorized_map/vectorized_map_lambda.keras", "absolute_path": "/workspace/keras_native/hf_keras_native_nonlayer_lambda_poc/variants/vectorized_map", "modelscan_version": "0.8.8", "timestamp": "2026-05-12T15:25:32.378590", "scanned": {"total_scanned": 1, "scanned_files": ["vectorized_map_lambda.keras"]}, "skipped": {"total_skipped": 3, "skipped_files": [{"category": "SCAN_NOT_SUPPORTED", "description": "Model Scan did not scan file", "source": "vectorized_map_lambda.keras:metadata.json"}, {"category": "SCAN_NOT_SUPPORTED", "description": "Model Scan did not scan file", "source": "vectorized_map_lambda.keras:config.json"}, {"category": "MODEL_CONFIG", "description": "Model Config not found", "source": "vectorized_map_lambda.keras:model.weights.h5"}]}}, "issues": [], "errors": []}
|
verify_all.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import hashlib
|
| 4 |
+
import json
|
| 5 |
+
import os
|
| 6 |
+
import subprocess
|
| 7 |
+
import sys
|
| 8 |
+
import zipfile
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
os.environ.setdefault("KERAS_BACKEND", "tensorflow")
|
| 12 |
+
os.environ.setdefault("TF_CPP_MIN_LOG_LEVEL", "2")
|
| 13 |
+
|
| 14 |
+
import keras
|
| 15 |
+
import numpy as np
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
ROOT = Path(__file__).resolve().parent
|
| 19 |
+
|
| 20 |
+
VARIANTS = [
|
| 21 |
+
{
|
| 22 |
+
"name": "activation",
|
| 23 |
+
"artifact": ROOT / "variants" / "activation" / "activation_lambda_manual_config.keras",
|
| 24 |
+
"marker": ROOT / "variants" / "activation" / "activation_lambda_marker.txt",
|
| 25 |
+
"expected_marker": "KERAS_NATIVE_ACTIVATION_LAMBDA_MARKER",
|
| 26 |
+
"inference_input": np.array([[2.0]], dtype="float32"),
|
| 27 |
+
},
|
| 28 |
+
{
|
| 29 |
+
"name": "ops_map",
|
| 30 |
+
"artifact": ROOT / "variants" / "ops_map" / "ops_map_inbound_lambda.keras",
|
| 31 |
+
"marker": ROOT / "variants" / "ops_map" / "ops_map_marker.txt",
|
| 32 |
+
"expected_marker": "OPS_MAP_MARKER",
|
| 33 |
+
"inference_input": np.ones((1, 2, 1), dtype="float32"),
|
| 34 |
+
},
|
| 35 |
+
{
|
| 36 |
+
"name": "vectorized_map",
|
| 37 |
+
"artifact": ROOT / "variants" / "vectorized_map" / "vectorized_map_lambda.keras",
|
| 38 |
+
"marker": ROOT / "variants" / "vectorized_map" / "vectorized_map_marker.txt",
|
| 39 |
+
"expected_marker": "VECTORIZED_MAP_MARKER",
|
| 40 |
+
"inference_input": np.ones((1, 2, 1), dtype="float32"),
|
| 41 |
+
},
|
| 42 |
+
]
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def sha256(path: Path) -> str:
|
| 46 |
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def lambda_paths(path: Path) -> list[str]:
|
| 50 |
+
with zipfile.ZipFile(path) as zf:
|
| 51 |
+
config = json.loads(zf.read("config.json"))
|
| 52 |
+
hits: list[str] = []
|
| 53 |
+
|
| 54 |
+
def walk(value, trail: str = "$") -> None:
|
| 55 |
+
if isinstance(value, dict):
|
| 56 |
+
if value.get("class_name") == "__lambda__":
|
| 57 |
+
hits.append(trail)
|
| 58 |
+
for key, child in value.items():
|
| 59 |
+
walk(child, f"{trail}.{key}")
|
| 60 |
+
elif isinstance(value, list):
|
| 61 |
+
for index, child in enumerate(value):
|
| 62 |
+
walk(child, f"{trail}[{index}]")
|
| 63 |
+
|
| 64 |
+
walk(config)
|
| 65 |
+
return hits
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def run_modelscan(artifact: Path) -> dict[str, object]:
|
| 69 |
+
output_path = artifact.with_name(f"{artifact.stem}.modelscan.json")
|
| 70 |
+
output_path.unlink(missing_ok=True)
|
| 71 |
+
proc = subprocess.run(
|
| 72 |
+
[
|
| 73 |
+
sys.executable,
|
| 74 |
+
"-m",
|
| 75 |
+
"modelscan.cli",
|
| 76 |
+
"-p",
|
| 77 |
+
str(artifact),
|
| 78 |
+
"-r",
|
| 79 |
+
"json",
|
| 80 |
+
"-o",
|
| 81 |
+
str(output_path),
|
| 82 |
+
"--show-skipped",
|
| 83 |
+
],
|
| 84 |
+
text=True,
|
| 85 |
+
capture_output=True,
|
| 86 |
+
check=False,
|
| 87 |
+
timeout=60,
|
| 88 |
+
)
|
| 89 |
+
parsed: dict[str, object]
|
| 90 |
+
try:
|
| 91 |
+
parsed = json.loads(output_path.read_text())
|
| 92 |
+
except (json.JSONDecodeError, FileNotFoundError):
|
| 93 |
+
parsed = {"stdout": proc.stdout, "stderr": proc.stderr}
|
| 94 |
+
parsed["_returncode"] = proc.returncode
|
| 95 |
+
parsed["_stderr"] = proc.stderr
|
| 96 |
+
return parsed
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def verify_variant(variant: dict[str, object]) -> dict[str, object]:
|
| 100 |
+
artifact = Path(variant["artifact"])
|
| 101 |
+
marker = Path(variant["marker"])
|
| 102 |
+
expected_marker = str(variant["expected_marker"])
|
| 103 |
+
inference_input = variant["inference_input"]
|
| 104 |
+
|
| 105 |
+
marker.unlink(missing_ok=True)
|
| 106 |
+
old_cwd = Path.cwd()
|
| 107 |
+
os.chdir(artifact.parent)
|
| 108 |
+
try:
|
| 109 |
+
result: dict[str, object] = {
|
| 110 |
+
"name": variant["name"],
|
| 111 |
+
"artifact": str(artifact.relative_to(ROOT)),
|
| 112 |
+
"sha256": sha256(artifact),
|
| 113 |
+
"lambda_paths": lambda_paths(artifact),
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
try:
|
| 117 |
+
keras.saving.load_model(artifact, safe_mode=True)
|
| 118 |
+
result["safe_mode_true"] = "loaded_unexpectedly"
|
| 119 |
+
except Exception as exc: # noqa: BLE001
|
| 120 |
+
result["safe_mode_true"] = (
|
| 121 |
+
f"blocked:{type(exc).__name__}:{str(exc).splitlines()[0]}"
|
| 122 |
+
)
|
| 123 |
+
result["marker_after_safe_mode_true"] = marker.exists()
|
| 124 |
+
|
| 125 |
+
marker.unlink(missing_ok=True)
|
| 126 |
+
model = keras.saving.load_model(artifact, safe_mode=False)
|
| 127 |
+
result["marker_after_safe_mode_false_load"] = marker.exists()
|
| 128 |
+
if not marker.exists():
|
| 129 |
+
model(inference_input)
|
| 130 |
+
result["marker_after_unsafe_inference"] = marker.exists()
|
| 131 |
+
result["marker_content"] = marker.read_text().strip() if marker.exists() else ""
|
| 132 |
+
result["marker_matched"] = result["marker_content"] == expected_marker
|
| 133 |
+
|
| 134 |
+
scan = run_modelscan(artifact)
|
| 135 |
+
summary = scan.get("summary", {}) if isinstance(scan, dict) else {}
|
| 136 |
+
result["modelscan_returncode"] = scan.get("_returncode")
|
| 137 |
+
result["modelscan_version"] = (
|
| 138 |
+
summary.get("modelscan_version") if isinstance(summary, dict) else None
|
| 139 |
+
)
|
| 140 |
+
result["modelscan_total_issues"] = (
|
| 141 |
+
summary.get("total_issues") if isinstance(summary, dict) else None
|
| 142 |
+
)
|
| 143 |
+
result["modelscan_issues"] = scan.get("issues", [])
|
| 144 |
+
result["passed"] = (
|
| 145 |
+
str(result["safe_mode_true"]).startswith("blocked:")
|
| 146 |
+
and result["marker_after_safe_mode_true"] is False
|
| 147 |
+
and result["marker_matched"] is True
|
| 148 |
+
and result["modelscan_total_issues"] == 0
|
| 149 |
+
)
|
| 150 |
+
return result
|
| 151 |
+
finally:
|
| 152 |
+
os.chdir(old_cwd)
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
def main() -> int:
|
| 156 |
+
results = {
|
| 157 |
+
"python": sys.version.split()[0],
|
| 158 |
+
"keras": keras.__version__,
|
| 159 |
+
"variants": [verify_variant(variant) for variant in VARIANTS],
|
| 160 |
+
}
|
| 161 |
+
results["all_passed"] = all(item["passed"] for item in results["variants"])
|
| 162 |
+
print(json.dumps(results, indent=2, sort_keys=True))
|
| 163 |
+
return 0 if results["all_passed"] else 1
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
if __name__ == "__main__":
|
| 167 |
+
raise SystemExit(main())
|