pragnyanramtha commited on
Commit
85e09c7
·
verified ·
1 Parent(s): dc7a5ac

Upload folder using huggingface_hub

Browse files
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())