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

1""" 

2Android SAST Tool (Static Application Security Testing) 

3 

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 

9 

10Secure mobile apps from the start. 

11""" 

12 

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 

19 

20from alprina_cli.tools.base import AlprinaToolBase, ToolOk, ToolError 

21 

22 

23class AndroidSASTParams(BaseModel): 

24 """ 

25 Parameters for Android SAST operations. 

26 

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 ) 

44 

45 

46class AndroidSASTTool(AlprinaToolBase[AndroidSASTParams]): 

47 """ 

48 Android SAST tool for mobile app security analysis. 

49 

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 

55 

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 

63 

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 """ 

74 

75 name: str = "AndroidSAST" 

76 description: str = """Android Static Application Security Testing. 

77 

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 

85 

86Returns: Structured Android security findings""" 

87 params: type[AndroidSASTParams] = AndroidSASTParams 

88 

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 ] 

98 

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 } 

107 

108 async def execute(self, params: AndroidSASTParams) -> ToolOk | ToolError: 

109 """ 

110 Execute Android SAST analysis. 

111 

112 Context: Returns structured mobile security findings. 

113 """ 

114 logger.info(f"AndroidSAST: {params.target} (type={params.analysis_type})") 

115 

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") 

125 

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) 

139 

140 # Filter by severity 

141 findings = self._filter_by_severity(findings, params.severity_threshold) 

142 

143 # Limit findings 

144 if len(findings) > params.max_findings: 

145 findings = findings[:params.max_findings] 

146 truncated = True 

147 else: 

148 truncated = False 

149 

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 

155 

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 } 

166 

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 ) 

174 

175 return ToolOk(content=result_content) 

176 

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 ) 

183 

184 async def _manifest_analysis(self, params: AndroidSASTParams) -> List[Dict[str, Any]]: 

185 """ 

186 Analyze AndroidManifest.xml. 

187 

188 Context: Security analysis of manifest file. 

189 """ 

190 findings = [] 

191 

192 target_path = Path(params.target).expanduser() 

193 

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] 

203 

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 

212 

213 try: 

214 tree = ET.parse(manifest_path) 

215 root = tree.getroot() 

216 

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 }) 

227 

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 }) 

238 

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 }) 

250 

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 }) 

259 

260 return findings 

261 

262 async def _permission_analysis(self, params: AndroidSASTParams) -> List[Dict[str, Any]]: 

263 """ 

264 Analyze app permissions. 

265 

266 Context: Permission auditing and risk assessment. 

267 """ 

268 findings = [] 

269 

270 target_path = Path(params.target).expanduser() 

271 

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] 

280 

281 if not manifest_path: 

282 return findings 

283 

284 try: 

285 tree = ET.parse(manifest_path) 

286 root = tree.getroot() 

287 

288 # Get all permissions 

289 permissions = root.findall(".//uses-permission", {'android': 'http://schemas.android.com/apk/res/android'}) 

290 

291 dangerous_perms = [] 

292 for perm in permissions: 

293 perm_name = perm.get('{http://schemas.android.com/apk/res/android}name', '') 

294 

295 # Check if dangerous 

296 for dangerous in self.DANGEROUS_PERMISSIONS: 

297 if dangerous in perm_name: 

298 dangerous_perms.append(perm_name) 

299 

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 }) 

310 

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 }) 

321 

322 except Exception as e: 

323 logger.debug(f"Permission analysis error: {e}") 

324 

325 return findings 

326 

327 async def _code_review_analysis(self, params: AndroidSASTParams) -> List[Dict[str, Any]]: 

328 """ 

329 Review source code for vulnerabilities. 

330 

331 Context: Code-level security analysis. 

332 """ 

333 findings = [] 

334 

335 target_path = Path(params.target).expanduser() 

336 

337 if not target_path.exists(): 

338 return findings 

339 

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]) 

345 

346 for code_file in code_files: 

347 try: 

348 content = code_file.read_text(errors="ignore") 

349 

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 }) 

363 

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 }) 

374 

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 }) 

385 

386 except Exception as e: 

387 logger.debug(f"Could not review {code_file}: {e}") 

388 

389 return findings 

390 

391 async def _crypto_analysis(self, params: AndroidSASTParams) -> List[Dict[str, Any]]: 

392 """ 

393 Analyze cryptographic implementations. 

394 

395 Context: Crypto security review. 

396 """ 

397 findings = [] 

398 

399 target_path = Path(params.target).expanduser() 

400 

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]) 

404 

405 for code_file in code_files: 

406 try: 

407 content = code_file.read_text(errors="ignore") 

408 

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 }) 

420 

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 }) 

431 

432 except Exception: 

433 pass 

434 

435 return findings 

436 

437 async def _network_analysis(self, params: AndroidSASTParams) -> List[Dict[str, Any]]: 

438 """ 

439 Analyze network security. 

440 

441 Context: Network communication security. 

442 """ 

443 findings = [] 

444 

445 target_path = Path(params.target).expanduser() 

446 

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 }) 

458 

459 # Check for cleartext traffic 

460 code_files = list(target_path.rglob("*.java"))[:20] 

461 code_files.extend(list(target_path.rglob("*.kt"))[:20]) 

462 

463 for code_file in code_files: 

464 try: 

465 content = code_file.read_text(errors="ignore") 

466 

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 }) 

476 

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 }) 

486 

487 except Exception: 

488 pass 

489 

490 return findings 

491 

492 async def _full_analysis(self, params: AndroidSASTParams) -> List[Dict[str, Any]]: 

493 """ 

494 Comprehensive Android security analysis. 

495 

496 Context: Full SAST scan. 

497 """ 

498 findings = [] 

499 

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)) 

506 

507 return findings 

508 

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) 

513 

514 return [ 

515 f for f in findings 

516 if severity_order.get(f.get("severity", "INFO"), 0) >= threshold_level 

517 ]