Coverage for src/alprina_cli/tools/security/android_sast.py: 15%
179 statements
« prev ^ index » next coverage.py v7.11.3, created at 2025-11-14 11:27 +0100
« prev ^ index » next coverage.py v7.11.3, created at 2025-11-14 11:27 +0100
1"""
2Android SAST Tool (Static Application Security Testing)
4Context Engineering:
5- Android app security analysis
6- Manifest analysis, code review, permission auditing
7- Returns structured mobile security findings
8- Memory-aware: Tracks app vulnerability patterns
10Secure mobile apps from the start.
11"""
13from typing import Dict, Any, List, Literal
14from pydantic import BaseModel, Field
15from loguru import logger
16from pathlib import Path
17import re
18import xml.etree.ElementTree as ET
20from alprina_cli.tools.base import AlprinaToolBase, ToolOk, ToolError
23class AndroidSASTParams(BaseModel):
24 """
25 Parameters for Android SAST operations.
27 Context: Focused schema for Android security testing.
28 """
29 target: str = Field(
30 description="Target Android app (APK, source code directory, or AndroidManifest.xml)"
31 )
32 analysis_type: Literal["manifest", "permissions", "code_review", "crypto", "network", "full"] = Field(
33 default="full",
34 description="Analysis type: manifest, permissions, code_review, crypto, network, full"
35 )
36 severity_threshold: Literal["INFO", "LOW", "MEDIUM", "HIGH", "CRITICAL"] = Field(
37 default="MEDIUM",
38 description="Minimum severity to report"
39 )
40 max_findings: int = Field(
41 default=50,
42 description="Maximum findings to return"
43 )
46class AndroidSASTTool(AlprinaToolBase[AndroidSASTParams]):
47 """
48 Android SAST tool for mobile app security analysis.
50 Context Engineering Benefits:
51 - Structured mobile security findings
52 - Manifest and permission analysis
53 - Code-level vulnerability detection
54 - Memory integration for pattern tracking
56 Analysis Types:
57 - manifest: AndroidManifest.xml analysis
58 - permissions: Permission auditing
59 - code_review: Source code security review
60 - crypto: Cryptographic implementation review
61 - network: Network security analysis
62 - full: Comprehensive security analysis
64 Usage:
65 ```python
66 tool = AndroidSASTTool(memory_service=memory)
67 result = await tool.execute(AndroidSASTParams(
68 target="./app/src",
69 analysis_type="full",
70 severity_threshold="HIGH"
71 ))
72 ```
73 """
75 name: str = "AndroidSAST"
76 description: str = """Android Static Application Security Testing.
78Capabilities:
79- AndroidManifest.xml security analysis
80- Permission auditing and risk assessment
81- Source code vulnerability detection
82- Cryptographic implementation review
83- Network security analysis
84- Comprehensive mobile security assessment
86Returns: Structured Android security findings"""
87 params: type[AndroidSASTParams] = AndroidSASTParams
89 # Dangerous Android permissions
90 DANGEROUS_PERMISSIONS = [
91 "READ_CONTACTS", "WRITE_CONTACTS",
92 "READ_SMS", "SEND_SMS", "RECEIVE_SMS",
93 "READ_CALL_LOG", "WRITE_CALL_LOG",
94 "CAMERA", "RECORD_AUDIO",
95 "ACCESS_FINE_LOCATION", "ACCESS_COARSE_LOCATION",
96 "READ_EXTERNAL_STORAGE", "WRITE_EXTERNAL_STORAGE"
97 ]
99 # Suspicious API calls
100 SUSPICIOUS_APIS = {
101 "reflection": ["Class.forName", "getDeclaredMethod", "invoke"],
102 "native_code": ["System.loadLibrary", "Runtime.exec"],
103 "crypto_weak": ["DES", "MD5", "SHA1"],
104 "network": ["HttpURLConnection", "OkHttp", "Retrofit"],
105 "data_storage": ["SharedPreferences", "SQLiteDatabase", "openFileOutput"]
106 }
108 async def execute(self, params: AndroidSASTParams) -> ToolOk | ToolError:
109 """
110 Execute Android SAST analysis.
112 Context: Returns structured mobile security findings.
113 """
114 logger.info(f"AndroidSAST: {params.target} (type={params.analysis_type})")
116 try:
117 # Check memory for similar apps
118 if self.memory_service and self.memory_service.is_enabled():
119 similar_apps = self.memory_service.search(
120 f"Android security analysis similar to {params.target}",
121 limit=3
122 )
123 if similar_apps:
124 logger.info(f"Found {len(similar_apps)} similar app analyses")
126 # Execute analysis
127 if params.analysis_type == "manifest":
128 findings = await self._manifest_analysis(params)
129 elif params.analysis_type == "permissions":
130 findings = await self._permission_analysis(params)
131 elif params.analysis_type == "code_review":
132 findings = await self._code_review_analysis(params)
133 elif params.analysis_type == "crypto":
134 findings = await self._crypto_analysis(params)
135 elif params.analysis_type == "network":
136 findings = await self._network_analysis(params)
137 else: # full
138 findings = await self._full_analysis(params)
140 # Filter by severity
141 findings = self._filter_by_severity(findings, params.severity_threshold)
143 # Limit findings
144 if len(findings) > params.max_findings:
145 findings = findings[:params.max_findings]
146 truncated = True
147 else:
148 truncated = False
150 # Calculate stats
151 severity_counts = {}
152 for finding in findings:
153 sev = finding.get("severity", "INFO")
154 severity_counts[sev] = severity_counts.get(sev, 0) + 1
156 result_content = {
157 "target": params.target,
158 "analysis_type": params.analysis_type,
159 "findings": findings,
160 "summary": {
161 "total_findings": len(findings),
162 "by_severity": severity_counts,
163 "truncated": truncated
164 }
165 }
167 # Store in memory
168 if self.memory_service and self.memory_service.is_enabled():
169 self.memory_service.add_scan_results(
170 tool_name="AndroidSAST",
171 target=params.target,
172 results=result_content
173 )
175 return ToolOk(content=result_content)
177 except Exception as e:
178 logger.error(f"Android SAST failed: {e}")
179 return ToolError(
180 message=f"Android SAST failed: {str(e)}",
181 brief="Analysis failed"
182 )
184 async def _manifest_analysis(self, params: AndroidSASTParams) -> List[Dict[str, Any]]:
185 """
186 Analyze AndroidManifest.xml.
188 Context: Security analysis of manifest file.
189 """
190 findings = []
192 target_path = Path(params.target).expanduser()
194 # Find AndroidManifest.xml
195 manifest_path = None
196 if target_path.is_file() and target_path.name == "AndroidManifest.xml":
197 manifest_path = target_path
198 elif target_path.is_dir():
199 # Search for manifest
200 manifests = list(target_path.rglob("AndroidManifest.xml"))
201 if manifests:
202 manifest_path = manifests[0]
204 if not manifest_path:
205 findings.append({
206 "category": "manifest",
207 "severity": "INFO",
208 "title": "No AndroidManifest.xml found",
209 "description": "Could not locate AndroidManifest.xml file"
210 })
211 return findings
213 try:
214 tree = ET.parse(manifest_path)
215 root = tree.getroot()
217 # Check for debuggable flag
218 if root.find(".//application[@android:debuggable='true']", {'android': 'http://schemas.android.com/apk/res/android'}):
219 findings.append({
220 "category": "manifest",
221 "severity": "HIGH",
222 "title": "Debuggable Flag Enabled",
223 "description": "Application is debuggable in production",
224 "recommendation": "Remove android:debuggable=\"true\" from manifest",
225 "file": str(manifest_path)
226 })
228 # Check for backup enabled
229 if root.find(".//application[@android:allowBackup='true']", {'android': 'http://schemas.android.com/apk/res/android'}):
230 findings.append({
231 "category": "manifest",
232 "severity": "MEDIUM",
233 "title": "Backup Enabled",
234 "description": "Application allows backup (potential data exposure)",
235 "recommendation": "Consider android:allowBackup=\"false\"",
236 "file": str(manifest_path)
237 })
239 # Check for exported components
240 exported_components = root.findall(".//*[@android:exported='true']", {'android': 'http://schemas.android.com/apk/res/android'})
241 if exported_components:
242 findings.append({
243 "category": "manifest",
244 "severity": "MEDIUM",
245 "title": "Exported Components",
246 "description": f"Found {len(exported_components)} exported components",
247 "recommendation": "Review exported components for necessity",
248 "file": str(manifest_path)
249 })
251 except Exception as e:
252 findings.append({
253 "category": "manifest",
254 "severity": "LOW",
255 "title": "Manifest Parse Error",
256 "description": f"Could not parse manifest: {str(e)}",
257 "file": str(manifest_path)
258 })
260 return findings
262 async def _permission_analysis(self, params: AndroidSASTParams) -> List[Dict[str, Any]]:
263 """
264 Analyze app permissions.
266 Context: Permission auditing and risk assessment.
267 """
268 findings = []
270 target_path = Path(params.target).expanduser()
272 # Find AndroidManifest.xml
273 manifest_path = None
274 if target_path.is_file() and target_path.name == "AndroidManifest.xml":
275 manifest_path = target_path
276 elif target_path.is_dir():
277 manifests = list(target_path.rglob("AndroidManifest.xml"))
278 if manifests:
279 manifest_path = manifests[0]
281 if not manifest_path:
282 return findings
284 try:
285 tree = ET.parse(manifest_path)
286 root = tree.getroot()
288 # Get all permissions
289 permissions = root.findall(".//uses-permission", {'android': 'http://schemas.android.com/apk/res/android'})
291 dangerous_perms = []
292 for perm in permissions:
293 perm_name = perm.get('{http://schemas.android.com/apk/res/android}name', '')
295 # Check if dangerous
296 for dangerous in self.DANGEROUS_PERMISSIONS:
297 if dangerous in perm_name:
298 dangerous_perms.append(perm_name)
300 if dangerous_perms:
301 findings.append({
302 "category": "permissions",
303 "severity": "HIGH",
304 "title": "Dangerous Permissions Requested",
305 "description": f"App requests {len(dangerous_perms)} dangerous permissions",
306 "permissions": dangerous_perms,
307 "recommendation": "Verify all permissions are necessary",
308 "file": str(manifest_path)
309 })
311 # Check for over-permission
312 if len(permissions) > 10:
313 findings.append({
314 "category": "permissions",
315 "severity": "MEDIUM",
316 "title": "Excessive Permissions",
317 "description": f"App requests {len(permissions)} permissions",
318 "recommendation": "Review and minimize permission requests",
319 "file": str(manifest_path)
320 })
322 except Exception as e:
323 logger.debug(f"Permission analysis error: {e}")
325 return findings
327 async def _code_review_analysis(self, params: AndroidSASTParams) -> List[Dict[str, Any]]:
328 """
329 Review source code for vulnerabilities.
331 Context: Code-level security analysis.
332 """
333 findings = []
335 target_path = Path(params.target).expanduser()
337 if not target_path.exists():
338 return findings
340 # Find Java/Kotlin files
341 code_files = []
342 if target_path.is_dir():
343 code_files.extend(list(target_path.rglob("*.java"))[:20])
344 code_files.extend(list(target_path.rglob("*.kt"))[:20])
346 for code_file in code_files:
347 try:
348 content = code_file.read_text(errors="ignore")
350 # Check for suspicious APIs
351 for api_category, api_list in self.SUSPICIOUS_APIS.items():
352 for api in api_list:
353 if api in content:
354 severity = "HIGH" if api_category in ["reflection", "native_code"] else "MEDIUM"
355 findings.append({
356 "category": "code_review",
357 "severity": severity,
358 "title": f"Suspicious API: {api}",
359 "description": f"Found {api_category} API: {api}",
360 "file": str(code_file),
361 "api_category": api_category
362 })
364 # Check for hardcoded secrets
365 if re.search(r"(api[_-]?key|secret|password)\s*=\s*['\"][^'\"]{10,}['\"]", content, re.IGNORECASE):
366 findings.append({
367 "category": "code_review",
368 "severity": "CRITICAL",
369 "title": "Hardcoded Secret",
370 "description": "Found hardcoded API key or secret",
371 "file": str(code_file),
372 "recommendation": "Use Android Keystore or encrypted storage"
373 })
375 # Check for SQL injection
376 if re.search(r"(execSQL|rawQuery).*\+.*", content):
377 findings.append({
378 "category": "code_review",
379 "severity": "HIGH",
380 "title": "Potential SQL Injection",
381 "description": "SQL query with string concatenation",
382 "file": str(code_file),
383 "recommendation": "Use parameterized queries"
384 })
386 except Exception as e:
387 logger.debug(f"Could not review {code_file}: {e}")
389 return findings
391 async def _crypto_analysis(self, params: AndroidSASTParams) -> List[Dict[str, Any]]:
392 """
393 Analyze cryptographic implementations.
395 Context: Crypto security review.
396 """
397 findings = []
399 target_path = Path(params.target).expanduser()
401 if target_path.is_dir():
402 code_files = list(target_path.rglob("*.java"))[:20]
403 code_files.extend(list(target_path.rglob("*.kt"))[:20])
405 for code_file in code_files:
406 try:
407 content = code_file.read_text(errors="ignore")
409 # Check for weak crypto
410 for weak_algo in ["DES", "MD5", "SHA1"]:
411 if weak_algo in content:
412 findings.append({
413 "category": "crypto",
414 "severity": "HIGH",
415 "title": f"Weak Cryptography: {weak_algo}",
416 "description": f"Using weak algorithm: {weak_algo}",
417 "file": str(code_file),
418 "recommendation": "Use AES-256, SHA-256 or better"
419 })
421 # Check for hardcoded keys
422 if re.search(r"(SecretKeySpec|IvParameterSpec).*new.*byte\[\]", content):
423 findings.append({
424 "category": "crypto",
425 "severity": "CRITICAL",
426 "title": "Hardcoded Encryption Key",
427 "description": "Encryption key appears to be hardcoded",
428 "file": str(code_file),
429 "recommendation": "Use Android Keystore"
430 })
432 except Exception:
433 pass
435 return findings
437 async def _network_analysis(self, params: AndroidSASTParams) -> List[Dict[str, Any]]:
438 """
439 Analyze network security.
441 Context: Network communication security.
442 """
443 findings = []
445 target_path = Path(params.target).expanduser()
447 if target_path.is_dir():
448 # Check network security config
449 network_configs = list(target_path.rglob("network_security_config.xml"))
450 if not network_configs:
451 findings.append({
452 "category": "network",
453 "severity": "MEDIUM",
454 "title": "No Network Security Config",
455 "description": "Missing network_security_config.xml",
456 "recommendation": "Implement network security configuration"
457 })
459 # Check for cleartext traffic
460 code_files = list(target_path.rglob("*.java"))[:20]
461 code_files.extend(list(target_path.rglob("*.kt"))[:20])
463 for code_file in code_files:
464 try:
465 content = code_file.read_text(errors="ignore")
467 if "http://" in content.lower() and "https://" not in content.lower():
468 findings.append({
469 "category": "network",
470 "severity": "HIGH",
471 "title": "Cleartext HTTP Traffic",
472 "description": "App uses HTTP instead of HTTPS",
473 "file": str(code_file),
474 "recommendation": "Use HTTPS for all network traffic"
475 })
477 # Check for certificate pinning
478 if "CertificatePinner" in content or "TrustManager" in content:
479 findings.append({
480 "category": "network",
481 "severity": "INFO",
482 "title": "Certificate Pinning Detected",
483 "description": "App implements certificate pinning (good practice)",
484 "file": str(code_file)
485 })
487 except Exception:
488 pass
490 return findings
492 async def _full_analysis(self, params: AndroidSASTParams) -> List[Dict[str, Any]]:
493 """
494 Comprehensive Android security analysis.
496 Context: Full SAST scan.
497 """
498 findings = []
500 # Execute all analysis types
501 findings.extend(await self._manifest_analysis(params))
502 findings.extend(await self._permission_analysis(params))
503 findings.extend(await self._code_review_analysis(params))
504 findings.extend(await self._crypto_analysis(params))
505 findings.extend(await self._network_analysis(params))
507 return findings
509 def _filter_by_severity(self, findings: List[Dict[str, Any]], threshold: str) -> List[Dict[str, Any]]:
510 """Filter findings by severity threshold"""
511 severity_order = {"INFO": 0, "LOW": 1, "MEDIUM": 2, "HIGH": 3, "CRITICAL": 4}
512 threshold_level = severity_order.get(threshold, 2)
514 return [
515 f for f in findings
516 if severity_order.get(f.get("severity", "INFO"), 0) >= threshold_level
517 ]