mobileguard 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,52 @@
1
+ # Copyright 2026 Jaspreet Singh
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ """MobileGuard — AI governance for consumer mobile platforms.
10
+
11
+ Reference implementation of the MobileGuard governance framework described in:
12
+ "MobileGuard: A Stack-Agnostic Governance Framework for Agentic AI
13
+ Across Consumer Mobile Delivery Platforms"
14
+ Jaspreet Singh · arXiv:XXXX.XXXXX · 2026
15
+
16
+ Four governance pillars:
17
+ PDQC — Pre-Deployment Quality Contracting (mobileguard contract)
18
+ TAC-M — Tiered Autonomy Calibration for Mobile (mobileguard tier)
19
+ PGSG — Platform Gatekeeper Simulation Gates (mobileguard scan)
20
+ AABE — Ambient Agent Boundary Enforcement (mobileguard scan)
21
+
22
+ MobileGuard does not collect telemetry, send analytics, or phone home.
23
+ All analysis is performed locally. The only outbound network calls are to
24
+ the Anthropic API when --llm is passed to `scan` or when running `contract`.
25
+ """
26
+
27
+ __version__ = "1.0.0"
28
+ __author__ = "Jaspreet Singh"
29
+ __license__ = "Apache-2.0"
30
+
31
+ from mobileguard.models import (
32
+ AuditReport,
33
+ ContractVerdict,
34
+ Finding,
35
+ Platform,
36
+ RuleCategory,
37
+ Severity,
38
+ ScanResult,
39
+ TierResult,
40
+ )
41
+
42
+ __all__ = [
43
+ "__version__",
44
+ "AuditReport",
45
+ "ContractVerdict",
46
+ "Finding",
47
+ "Platform",
48
+ "RuleCategory",
49
+ "Severity",
50
+ "ScanResult",
51
+ "TierResult",
52
+ ]
mobileguard/audit.py ADDED
@@ -0,0 +1,297 @@
1
+ # Copyright 2026 Jaspreet Singh
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ """PGSG — Platform Gatekeeper Simulation.
10
+
11
+ Generates structured compliance audit reports for mobileguard audit.
12
+ Maps to EU AI Act Article 50, Apple Guideline 5.1.2(i), and Google Play AI policy.
13
+ Supports markdown, JSON, and HTML output formats.
14
+ PDF export is planned for v1.1 — convert HTML output using your browser's print-to-PDF.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import re
21
+ from datetime import datetime, timezone
22
+ from pathlib import Path
23
+
24
+ from mobileguard import __version__
25
+ from mobileguard.models import AuditReport, Finding, Platform, RuleCategory, Severity, ScanResult
26
+
27
+ _AI_DOMAIN_RE = re.compile(
28
+ r"https?://(?:api\.openai\.com|api\.anthropic\.com|"
29
+ r"generativelanguage\.googleapis\.com|[\w\-]+\.openai\.azure\.com|"
30
+ r"api\.cohere\.ai|api\.mistral\.ai|api\.groq\.com)",
31
+ re.IGNORECASE,
32
+ )
33
+
34
+ _AI_FEATURE_PATTERNS = [
35
+ (re.compile(r"api\.anthropic\.com", re.I), "Anthropic Claude API (external)"),
36
+ (re.compile(r"api\.openai\.com", re.I), "OpenAI API (external)"),
37
+ (re.compile(r"generativelanguage\.googleapis\.com", re.I), "Google Gemini API (external)"),
38
+ (re.compile(r"openai\.azure\.com", re.I), "Azure OpenAI (external)"),
39
+ (re.compile(r"api\.cohere\.ai", re.I), "Cohere API (external)"),
40
+ (re.compile(r"MLKit|ml_kit", re.I), "Google ML Kit (on-device)"),
41
+ (re.compile(r"CoreML|\.mlmodel", re.I), "Core ML (on-device)"),
42
+ (re.compile(r"CreateML", re.I), "Create ML (on-device)"),
43
+ (re.compile(r"Vision\b", re.I), "Vision Framework (on-device)"),
44
+ (re.compile(r"NaturalLanguage\b", re.I), "Natural Language Framework (on-device)"),
45
+ (re.compile(r"GeminiNano|gemini[\-_]nano", re.I), "Gemini Nano (on-device)"),
46
+ ]
47
+
48
+ _FRAMEWORK_LABELS = {
49
+ RuleCategory.APP_STORE: "Apple Guideline 5.1.2(i)",
50
+ RuleCategory.GOOGLE_PLAY: "Google Play AI Policy",
51
+ RuleCategory.EU_AI_ACT: "EU AI Act Article 50",
52
+ RuleCategory.OWASP: "OWASP Mobile AI Top 5",
53
+ }
54
+
55
+
56
+ def generate_report(
57
+ scan_result: ScanResult,
58
+ *,
59
+ app_name: str,
60
+ version: str,
61
+ platforms: list[Platform],
62
+ include_evidence: bool = False,
63
+ ) -> AuditReport:
64
+ """Build an AuditReport from a ScanResult."""
65
+ ai_features = _detect_ai_features(scan_result)
66
+ compliance_status = _build_compliance_status(scan_result.findings)
67
+ attestation = (
68
+ f"This report was generated automatically by MobileGuard v{__version__} "
69
+ f"on {datetime.now(tz=timezone.utc).strftime('%Y-%m-%d')}. "
70
+ "It documents governance practices as detected in the codebase at the time of scan. "
71
+ "This report does not constitute legal advice."
72
+ )
73
+
74
+ findings = scan_result.findings
75
+ if not include_evidence:
76
+ findings = [f.model_copy(update={"evidence": None}) for f in findings]
77
+
78
+ return AuditReport(
79
+ app_name=app_name,
80
+ version=version,
81
+ platforms=platforms,
82
+ generated_at=datetime.now(tz=timezone.utc),
83
+ tool_version=__version__,
84
+ compliance_status=compliance_status,
85
+ ai_features=ai_features,
86
+ findings=findings,
87
+ attestation=attestation,
88
+ )
89
+
90
+
91
+ def render_markdown(report: AuditReport) -> str:
92
+ """Render an AuditReport as a Markdown document."""
93
+ lines: list[str] = []
94
+ platforms_str = ", ".join(p.value.title() for p in report.platforms)
95
+ lines += [
96
+ "# MobileGuard Governance Audit Report",
97
+ f"**App:** {report.app_name} · "
98
+ f"**Version:** {report.version} · "
99
+ f"**Date:** {report.generated_at.strftime('%Y-%m-%d')}",
100
+ f"**Platforms:** {platforms_str} · "
101
+ f"**Generated by:** MobileGuard v{report.tool_version}",
102
+ "",
103
+ "## Compliance Status",
104
+ "",
105
+ "| Framework | Status | Issues |",
106
+ "|---|---|---|",
107
+ ]
108
+
109
+ for category, status_info in report.compliance_status.items():
110
+ icon = status_info["icon"]
111
+ status = status_info["status"]
112
+ issues = status_info["issues"]
113
+ lines.append(f"| {category} | {icon} {status} | {issues} |")
114
+
115
+ lines += ["", "## AI Features Inventory", ""]
116
+ if report.ai_features:
117
+ for feature in report.ai_features:
118
+ lines.append(f"- {feature['name']} — detected in `{feature['file']}`")
119
+ else:
120
+ lines.append("- No AI integrations detected.")
121
+
122
+ # Findings by severity
123
+ criticals = [f for f in report.findings if f.severity == Severity.CRITICAL]
124
+ errors = [f for f in report.findings if f.severity == Severity.ERROR]
125
+ warnings = [f for f in report.findings if f.severity == Severity.WARNING]
126
+
127
+ lines += ["", "## Critical Issues Requiring Remediation", ""]
128
+ if criticals:
129
+ for f in criticals:
130
+ lines += [
131
+ f"### {f.rule_id}: {f.description}",
132
+ f"- **File:** `{f.file_path}`" + (f":{f.line_number}" if f.line_number else ""),
133
+ f"- **Fix:** {f.fix}",
134
+ f"- **Pillar:** {f.pillar}",
135
+ f"- **Reference:** {f.reference}" if f.reference else "",
136
+ "",
137
+ ]
138
+ else:
139
+ lines.append("No critical issues found.")
140
+
141
+ if errors:
142
+ lines += ["## Errors", ""]
143
+ for f in errors:
144
+ lines += [
145
+ f"### {f.rule_id}: {f.description}",
146
+ f"- **File:** `{f.file_path}`" + (f":{f.line_number}" if f.line_number else ""),
147
+ f"- **Fix:** {f.fix}",
148
+ "",
149
+ ]
150
+
151
+ if warnings:
152
+ lines += ["## Warnings", ""]
153
+ for f in warnings:
154
+ lines += [
155
+ f"- **{f.rule_id}** `{f.file_path}`"
156
+ + (f":{f.line_number}" if f.line_number else "")
157
+ + f" — {f.description}",
158
+ ]
159
+
160
+ lines += [
161
+ "",
162
+ "## Attestation",
163
+ "",
164
+ report.attestation,
165
+ ]
166
+
167
+ return "\n".join(lines)
168
+
169
+
170
+ def render_html(report: AuditReport) -> str:
171
+ """Render an AuditReport as an HTML document."""
172
+ md = render_markdown(report)
173
+ # Simple conversion: escape HTML in markdown and wrap in styled page
174
+ escaped = md.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
175
+ rows = []
176
+ for category, info in report.compliance_status.items():
177
+ rows.append(
178
+ f"<tr><td>{category}</td>"
179
+ f"<td>{info['icon']} {info['status']}</td>"
180
+ f"<td>{info['issues']}</td></tr>"
181
+ )
182
+
183
+ findings_html = ""
184
+ for f in report.findings:
185
+ sev_class = f.severity.value
186
+ loc = f"`{f.file_path}`" + (f":{f.line_number}" if f.line_number else "")
187
+ findings_html += (
188
+ f'<div class="finding {sev_class}">'
189
+ f"<strong>{f.rule_id}</strong> [{f.severity.value.upper()}] {f.description}<br>"
190
+ f"<small>File: {loc} · Fix: {f.fix}</small></div>\n"
191
+ )
192
+
193
+ return f"""<!DOCTYPE html>
194
+ <html lang="en">
195
+ <head>
196
+ <meta charset="utf-8">
197
+ <title>MobileGuard Audit Report — {report.app_name}</title>
198
+ <style>
199
+ body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
200
+ max-width: 900px; margin: 40px auto; padding: 0 20px; color: #1a1a1a; }}
201
+ h1 {{ color: #0052cc; }}
202
+ h2 {{ border-bottom: 2px solid #eee; padding-bottom: 8px; }}
203
+ table {{ border-collapse: collapse; width: 100%; }}
204
+ th, td {{ border: 1px solid #ddd; padding: 8px 12px; text-align: left; }}
205
+ th {{ background: #f5f5f5; }}
206
+ .finding {{ border-left: 4px solid #ccc; padding: 8px 12px; margin: 8px 0; border-radius: 4px; }}
207
+ .critical {{ border-color: #d32f2f; background: #ffebee; }}
208
+ .error {{ border-color: #f57c00; background: #fff3e0; }}
209
+ .warning {{ border-color: #f9a825; background: #fffde7; }}
210
+ .info {{ border-color: #1976d2; background: #e3f2fd; }}
211
+ .attestation {{ font-style: italic; color: #666; border-top: 1px solid #eee; padding-top: 16px; }}
212
+ </style>
213
+ </head>
214
+ <body>
215
+ <h1>MobileGuard Governance Audit Report</h1>
216
+ <p><strong>App:</strong> {report.app_name} &nbsp;|&nbsp;
217
+ <strong>Version:</strong> {report.version} &nbsp;|&nbsp;
218
+ <strong>Date:</strong> {report.generated_at.strftime('%Y-%m-%d')}<br>
219
+ <strong>Platforms:</strong> {', '.join(p.value.title() for p in report.platforms)} &nbsp;|&nbsp;
220
+ <strong>Generated by:</strong> MobileGuard v{report.tool_version}</p>
221
+
222
+ <h2>Compliance Status</h2>
223
+ <table>
224
+ <tr><th>Framework</th><th>Status</th><th>Issues</th></tr>
225
+ {''.join(rows)}
226
+ </table>
227
+
228
+ <h2>AI Features Inventory</h2>
229
+ <ul>
230
+ {''.join(f"<li>{feat['name']} — <code>{feat['file']}</code></li>" for feat in report.ai_features) or '<li>No AI integrations detected.</li>'}
231
+ </ul>
232
+
233
+ <h2>Findings</h2>
234
+ {findings_html or '<p>No findings.</p>'}
235
+
236
+ <div class="attestation"><h2>Attestation</h2><p>{report.attestation}</p></div>
237
+ </body>
238
+ </html>"""
239
+
240
+
241
+ def render_json(report: AuditReport) -> str:
242
+ """Render an AuditReport as JSON."""
243
+ return report.model_dump_json(indent=2)
244
+
245
+
246
+ def _detect_ai_features(scan_result: ScanResult) -> list[dict[str, str]]:
247
+ """Infer AI feature inventory from the scan result's project path."""
248
+ seen: set[str] = set()
249
+ features: list[dict[str, str]] = []
250
+ root = Path(scan_result.project_path)
251
+
252
+ for file_path in root.rglob("*"):
253
+ if not file_path.is_file():
254
+ continue
255
+ try:
256
+ content = file_path.read_text(encoding="utf-8", errors="replace")
257
+ except OSError:
258
+ continue
259
+
260
+ for pattern, label in _AI_FEATURE_PATTERNS:
261
+ if pattern.search(content) and label not in seen:
262
+ seen.add(label)
263
+ rel = str(file_path.relative_to(root))
264
+ features.append({"name": label, "file": rel})
265
+
266
+ return features
267
+
268
+
269
+ def _build_compliance_status(findings: list[Finding]) -> dict[str, dict[str, object]]:
270
+ """Build per-framework compliance status from findings."""
271
+ category_findings: dict[RuleCategory, list[Finding]] = {cat: [] for cat in RuleCategory}
272
+ for f in findings:
273
+ category_findings[f.category].append(f)
274
+
275
+ status: dict[str, dict[str, object]] = {}
276
+ for category, label in _FRAMEWORK_LABELS.items():
277
+ cat_findings = category_findings[category]
278
+ critical_count = sum(1 for f in cat_findings if f.severity == Severity.CRITICAL)
279
+ error_count = sum(1 for f in cat_findings if f.severity == Severity.ERROR)
280
+ warning_count = sum(1 for f in cat_findings if f.severity == Severity.WARNING)
281
+
282
+ if critical_count > 0:
283
+ icon, compliance = "❌", "Non-compliant"
284
+ issues = f"{critical_count} critical"
285
+ elif error_count > 0:
286
+ icon, compliance = "⚠", "Partial"
287
+ issues = f"{error_count} error{'s' if error_count > 1 else ''}"
288
+ elif warning_count > 0:
289
+ icon, compliance = "⚠", "Partial"
290
+ issues = f"{warning_count} warning{'s' if warning_count > 1 else ''}"
291
+ else:
292
+ icon, compliance = "✅", "Compliant"
293
+ issues = "0"
294
+
295
+ status[label] = {"icon": icon, "status": compliance, "issues": issues}
296
+
297
+ return status