-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmodel_scan.py
More file actions
326 lines (261 loc) · 10.4 KB
/
Copy pathmodel_scan.py
File metadata and controls
326 lines (261 loc) · 10.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
#!/usr/bin/env python3
"""
Prisma AIRS Model Security Scanner
Production-grade CI/CD integration for AI/ML model security scanning.
"""
import os
import sys
import argparse
import json
from typing import Dict, Any, Set
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
from model_security_client.api import ModelSecurityAPIClient
# Constants for policy enforcement
# The PANW model-security-client SDK returns eval_outcome as "ALLOWED" or
# "BLOCKED" only. Earlier example scripts and pre-GA documentation used
# values like "PASS", "CLEAN", or "SUCCESS"; those are no longer correct.
# Reference: https://docs.paloaltonetworks.com/ai-runtime-security/ai-model-security
ALLOWED_OUTCOMES: Set[str] = {"ALLOWED"}
DEFAULT_FAIL_SEVERITIES: str = "CRITICAL,HIGH"
# Exit codes
EXIT_SUCCESS = 0
EXIT_SECURITY_VIOLATION = 1
EXIT_ERROR = 2
def parse_arguments() -> argparse.Namespace:
"""Parses command-line arguments for model path and security group ID."""
parser = argparse.ArgumentParser(
description="Prisma AIRS Model Security Scan",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Scan Hugging Face model
python model_scan.py --model-path https://huggingface.co/bert-base-uncased --security-group-id <uuid>
# Scan local model file
python model_scan.py --model-path ./models/my_model.safetensors --security-group-id <uuid>
# Custom severity thresholds (via environment)
export FAIL_ON_SEVERITY="CRITICAL,HIGH,MEDIUM"
python model_scan.py --model-path <path> --security-group-id <uuid>
"""
)
parser.add_argument(
"--model-path",
required=True,
help="Path to model artifact (local file path or HTTPS URL)"
)
parser.add_argument(
"--security-group-id",
required=True,
help="UUID of the Prisma AIRS security group"
)
return parser.parse_args()
def validate_model_uri(model_path: str) -> str:
"""
Validates and converts model path to proper URI format.
Args:
model_path: Raw model path from user input
Returns:
Properly formatted model URI
Raises:
ValueError: If model_path uses insecure HTTP or invalid format
"""
# Remote URL validation
if model_path.startswith("https://"):
print(f" Target: Remote URL ({model_path})")
return model_path
# Reject insecure HTTP - prevents downgrade attacks
if model_path.startswith("http://"):
raise ValueError(
"HTTP URLs are not allowed for security reasons. "
"Use HTTPS instead (Hugging Face models are HTTPS-only)."
)
# Local file path - convert to file:// URI
abs_path = os.path.abspath(model_path)
if not os.path.exists(abs_path):
raise ValueError(f"Local model file not found: {abs_path}")
print(f" Target: Local File ({abs_path})")
return f"file://{abs_path}"
def get_severity_thresholds() -> Set[str]:
"""
Retrieves severity thresholds from environment or uses defaults.
Returns:
Set of severity levels that trigger pipeline failure
"""
severity_str = os.getenv("FAIL_ON_SEVERITY", DEFAULT_FAIL_SEVERITIES)
severities = {s.strip().upper() for s in severity_str.split(",")}
# Validate severity levels
valid_severities = {"CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"}
invalid = severities - valid_severities
if invalid:
print(f"⚠️ Warning: Invalid severity levels ignored: {invalid}")
severities = severities & valid_severities
print(f" Fail on Severities: {', '.join(sorted(severities))}")
return severities
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=4, max=10),
retry=retry_if_exception_type((ConnectionError, TimeoutError)),
reraise=True
)
def perform_scan_with_retry(
client: ModelSecurityAPIClient,
security_group_id: str,
model_uri: str
) -> Any:
"""
Performs model scan with automatic retry on transient failures.
Args:
client: Initialized ModelSecurityAPIClient
security_group_id: Security group UUID
model_uri: Model URI (file:// or https://)
Returns:
Scan result object from API
Raises:
ConnectionError, TimeoutError: On persistent network failures (after retries)
"""
print(" Initiating scan (with automatic retry on transient failures)...")
return client.scan(
security_group_uuid=security_group_id,
model_uri=model_uri
)
def serialize_scan_results(result: Any) -> Dict[str, Any]:
"""
Safely converts scan result object to dictionary.
Args:
result: Scan result object from API
Returns:
Dictionary representation of scan results
"""
try:
# Try Pydantic v2 model_dump() first
return result.model_dump()
except AttributeError:
try:
# Fall back to Pydantic v1 dict()
return result.dict()
except AttributeError:
# Last resort: direct __dict__ access
return result.__dict__
def save_scan_report(data_dict: Dict[str, Any], filename: str = "model_scan_report.json") -> None:
"""
Saves scan results as JSON artifact for CI/CD retention.
Args:
data_dict: Scan results as dictionary
filename: Output filename
"""
with open(filename, 'w') as f:
json.dump(data_dict, f, indent=4, default=str)
print(f" Report saved to '{filename}'")
def evaluate_scan_outcome(
result: Any,
data_dict: Dict[str, Any],
fail_severities: Set[str]
) -> bool:
"""
Evaluates scan results against security policy.
Args:
result: Raw scan result object
data_dict: Serialized scan results
fail_severities: Set of severity levels that trigger failure
Returns:
True if policy violated (should fail), False if passed
"""
policy_violated = False
# Check 1: Validate high-level scan outcome.
# The SDK exposes eval_outcome as a Python enum (EvalOutcome.ALLOWED /
# EvalOutcome.BLOCKED). str(enum) returns "EVALOUTCOME.ALLOWED" which
# would never match the bare "ALLOWED" we expect. Read the value from
# the serialized dict instead, which model_dump() flattens to the bare
# string. Fall back to enum normalization if the dict key is missing.
outcome_str = (
str(data_dict.get("eval_outcome", "")).upper()
or str(getattr(result.eval_outcome, "value", result.eval_outcome)).upper()
)
# Trim any "EVALOUTCOME." prefix that slips through (defensive only).
if "." in outcome_str:
outcome_str = outcome_str.rsplit(".", 1)[-1]
print(f"\n🏁 Scan Status: {outcome_str}")
if outcome_str not in ALLOWED_OUTCOMES:
print(f"⚠️ VIOLATION: eval_outcome is '{outcome_str}' (model BLOCKED by security policy)")
policy_violated = True
# Check 2: Deep inspection of individual findings
findings = data_dict.get("findings", [])
if findings:
print(f"\n🔍 Detailed Findings ({len(findings)} total):")
violation_count = 0
for finding in findings:
severity = str(finding.get('severity', 'UNKNOWN')).upper()
category = finding.get('category', 'Generic')
description = finding.get('description', 'No description')
# Check if this severity level should trigger failure
triggers_failure = severity in fail_severities
marker = "❌" if triggers_failure else "ℹ️"
print(f" {marker} [{severity}] {category}")
if triggers_failure:
print(f" └─ {description[:100]}{'...' if len(description) > 100 else ''}")
policy_violated = True
violation_count += 1
if violation_count > 0:
print(f"\n⚠️ Found {violation_count} finding(s) matching failure criteria")
else:
print("\n✅ No security findings detected")
return policy_violated
def run_model_scan(model_path: str, security_group_id: str) -> int:
"""
Main orchestration function for model security scanning.
Args:
model_path: Path to model (local or remote)
security_group_id: Security group UUID
Returns:
Exit code (0=success, 1=security violation, 2=error)
"""
try:
# Initialize configuration
base_url = os.getenv(
"MODEL_SECURITY_API_ENDPOINT",
"https://api.sase.paloaltonetworks.com/aims"
)
fail_severities = get_severity_thresholds()
print(f"🚀 Initializing Prisma AIRS Scanner")
print(f" Endpoint: {base_url}")
print(f" Profile UUID: {security_group_id}")
# Validate and prepare model URI
final_model_uri = validate_model_uri(model_path)
# Initialize API client
client = ModelSecurityAPIClient(base_url=base_url)
# Perform scan with automatic retry
result = perform_scan_with_retry(client, security_group_id, final_model_uri)
# Serialize and save results
data_dict = serialize_scan_results(result)
save_scan_report(data_dict)
# Evaluate against security policy
policy_violated = evaluate_scan_outcome(result, data_dict, fail_severities)
# Determine final status
if policy_violated:
print("\n⛔ SCAN FAILED: Security violations detected")
print(" Pipeline halted to prevent deployment of vulnerable model")
print(" Review 'model_scan_report.json' artifact for detailed findings")
return EXIT_SECURITY_VIOLATION
else:
print("\n✅ SCAN PASSED: Model meets security requirements")
print(" No violations found - safe to proceed with deployment")
return EXIT_SUCCESS
except ValueError as e:
# Configuration or validation errors
print(f"\n❌ CONFIGURATION ERROR: {e}")
print(" Fix the error and retry the scan")
return EXIT_ERROR
except Exception as e:
# Unexpected errors (API failures, network issues, etc.)
print(f"\n💥 CRITICAL ERROR: {e}")
print(" Check connectivity, credentials, and API endpoint")
# Print full traceback for debugging
import traceback
traceback.print_exc()
return EXIT_ERROR
def main() -> None:
"""Entry point for CLI execution."""
args = parse_arguments()
exit_code = run_model_scan(args.model_path, args.security_group_id)
sys.exit(exit_code)
if __name__ == "__main__":
main()