anais-apk-forensic 1.0.0
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.
- package/LICENSE +21 -0
- package/README.md +249 -0
- package/anais.sh +669 -0
- package/analysis_tools/__pycache__/apk_basic_info.cpython-313.pyc +0 -0
- package/analysis_tools/__pycache__/apk_basic_info.cpython-314.pyc +0 -0
- package/analysis_tools/__pycache__/check_zip_encryption.cpython-313.pyc +0 -0
- package/analysis_tools/__pycache__/check_zip_encryption.cpython-314.pyc +0 -0
- package/analysis_tools/__pycache__/detect_obfuscation.cpython-313.pyc +0 -0
- package/analysis_tools/__pycache__/detect_obfuscation.cpython-314.pyc +0 -0
- package/analysis_tools/__pycache__/dex_payload_hunter.cpython-314.pyc +0 -0
- package/analysis_tools/__pycache__/entropy_analyzer.cpython-314.pyc +0 -0
- package/analysis_tools/__pycache__/error_logger.cpython-313.pyc +0 -0
- package/analysis_tools/__pycache__/error_logger.cpython-314.pyc +0 -0
- package/analysis_tools/__pycache__/find_encrypted_payload.cpython-314.pyc +0 -0
- package/analysis_tools/__pycache__/fix_apk_headers.cpython-313.pyc +0 -0
- package/analysis_tools/__pycache__/fix_apk_headers.cpython-314.pyc +0 -0
- package/analysis_tools/__pycache__/manifest_analyzer.cpython-313.pyc +0 -0
- package/analysis_tools/__pycache__/manifest_analyzer.cpython-314.pyc +0 -0
- package/analysis_tools/__pycache__/network_analyzer.cpython-313.pyc +0 -0
- package/analysis_tools/__pycache__/network_analyzer.cpython-314.pyc +0 -0
- package/analysis_tools/__pycache__/report_generator.cpython-313.pyc +0 -0
- package/analysis_tools/__pycache__/report_generator.cpython-314.pyc +0 -0
- package/analysis_tools/__pycache__/report_generator_modular.cpython-314.pyc +0 -0
- package/analysis_tools/__pycache__/sast_scanner.cpython-313.pyc +0 -0
- package/analysis_tools/__pycache__/sast_scanner.cpython-314.pyc +0 -0
- package/analysis_tools/__pycache__/so_string_analyzer.cpython-314.pyc +0 -0
- package/analysis_tools/__pycache__/yara_enhanced_analyzer.cpython-314.pyc +0 -0
- package/analysis_tools/__pycache__/yara_results_processor.cpython-314.pyc +0 -0
- package/analysis_tools/apk_basic_info.py +85 -0
- package/analysis_tools/check_zip_encryption.py +142 -0
- package/analysis_tools/detect_obfuscation.py +650 -0
- package/analysis_tools/dex_payload_hunter.py +734 -0
- package/analysis_tools/entropy_analyzer.py +335 -0
- package/analysis_tools/error_logger.py +75 -0
- package/analysis_tools/find_encrypted_payload.py +485 -0
- package/analysis_tools/fix_apk_headers.py +154 -0
- package/analysis_tools/manifest_analyzer.py +214 -0
- package/analysis_tools/network_analyzer.py +287 -0
- package/analysis_tools/report_generator.py +506 -0
- package/analysis_tools/report_generator_modular.py +885 -0
- package/analysis_tools/sast_scanner.py +412 -0
- package/analysis_tools/so_string_analyzer.py +406 -0
- package/analysis_tools/yara_enhanced_analyzer.py +330 -0
- package/analysis_tools/yara_results_processor.py +368 -0
- package/analyzer_config.json +113 -0
- package/apkid/__init__.py +32 -0
- package/apkid/__pycache__/__init__.cpython-313.pyc +0 -0
- package/apkid/__pycache__/__init__.cpython-314.pyc +0 -0
- package/apkid/__pycache__/apkid.cpython-313.pyc +0 -0
- package/apkid/__pycache__/apkid.cpython-314.pyc +0 -0
- package/apkid/__pycache__/main.cpython-313.pyc +0 -0
- package/apkid/__pycache__/main.cpython-314.pyc +0 -0
- package/apkid/__pycache__/output.cpython-313.pyc +0 -0
- package/apkid/__pycache__/rules.cpython-313.pyc +0 -0
- package/apkid/apkid.py +266 -0
- package/apkid/main.py +98 -0
- package/apkid/output.py +177 -0
- package/apkid/rules/apk/common.yara +68 -0
- package/apkid/rules/apk/obfuscators.yara +118 -0
- package/apkid/rules/apk/packers.yara +1197 -0
- package/apkid/rules/apk/protectors.yara +301 -0
- package/apkid/rules/dex/abnormal.yara +104 -0
- package/apkid/rules/dex/anti-vm.yara +568 -0
- package/apkid/rules/dex/common.yara +60 -0
- package/apkid/rules/dex/compilers.yara +434 -0
- package/apkid/rules/dex/obfuscators.yara +602 -0
- package/apkid/rules/dex/packers.yara +761 -0
- package/apkid/rules/dex/protectors.yara +520 -0
- package/apkid/rules/dll/common.yara +38 -0
- package/apkid/rules/dll/obfuscators.yara +43 -0
- package/apkid/rules/elf/anti-vm.yara +43 -0
- package/apkid/rules/elf/common.yara +54 -0
- package/apkid/rules/elf/obfuscators.yara +991 -0
- package/apkid/rules/elf/packers.yara +1128 -0
- package/apkid/rules/elf/protectors.yara +794 -0
- package/apkid/rules/res/common.yara +43 -0
- package/apkid/rules/res/obfuscators.yara +46 -0
- package/apkid/rules/res/protectors.yara +46 -0
- package/apkid/rules.py +77 -0
- package/bin/anais +3 -0
- package/dist/cli.js +82 -0
- package/dist/index.js +123 -0
- package/dist/types/index.js +2 -0
- package/dist/utils/index.js +21 -0
- package/dist/utils/output.js +44 -0
- package/dist/utils/paths.js +107 -0
- package/docs/ARCHITECTURE.txt +353 -0
- package/docs/Workflow and Reference.md +445 -0
- package/package.json +70 -0
- package/rules/yara_general_rules.yar +323 -0
- package/scripts/dynamic_analysis_helper.sh +334 -0
- package/scripts/frida/dpt_dex_dumper.js +145 -0
- package/scripts/frida/frida_dex_dump.js +145 -0
- package/scripts/frida/frida_hooks.js +437 -0
- package/scripts/frida/frida_websocket_extractor.js +154 -0
- package/scripts/setup.sh +206 -0
- package/scripts/validate_framework.sh +224 -0
- package/src/cli.ts +91 -0
- package/src/index.ts +123 -0
- package/src/types/index.ts +44 -0
- package/src/utils/index.ts +6 -0
- package/src/utils/output.ts +50 -0
- package/src/utils/paths.ts +72 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,885 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Modular Report Generator
|
|
4
|
+
Generates split reports with main index and separate detail files
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
import json
|
|
9
|
+
import argparse
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
|
|
13
|
+
class ModularReportGenerator:
|
|
14
|
+
def __init__(self, workspace, temp_dir, apk_name, sha256):
|
|
15
|
+
self.workspace = Path(workspace)
|
|
16
|
+
self.temp_dir = Path(temp_dir)
|
|
17
|
+
self.apk_name = apk_name
|
|
18
|
+
self.sha256 = sha256
|
|
19
|
+
self.timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
20
|
+
self.report_dir = self.workspace / 'reports'
|
|
21
|
+
self.report_dir.mkdir(exist_ok=True)
|
|
22
|
+
|
|
23
|
+
# Load all analysis results
|
|
24
|
+
self.load_results()
|
|
25
|
+
|
|
26
|
+
def load_results(self):
|
|
27
|
+
"""Load all JSON results from temp directory"""
|
|
28
|
+
self.results = {}
|
|
29
|
+
|
|
30
|
+
result_files = {
|
|
31
|
+
'basic_info': 'basic_info.json',
|
|
32
|
+
'encryption_check': 'encryption_check.json',
|
|
33
|
+
'obfuscation': 'obfuscation_report.json',
|
|
34
|
+
'dex_payload': 'dex_payload_report.json',
|
|
35
|
+
'so_strings': 'so_strings.json',
|
|
36
|
+
'entropy': 'entropy_analysis.json',
|
|
37
|
+
'yara': 'yara_enhanced.json',
|
|
38
|
+
'sast': 'sast_findings.json',
|
|
39
|
+
'manifest': 'manifest_findings.json',
|
|
40
|
+
'network': 'network_findings.json',
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
for key, filename in result_files.items():
|
|
44
|
+
filepath = self.temp_dir / filename
|
|
45
|
+
if filepath.exists():
|
|
46
|
+
try:
|
|
47
|
+
with open(filepath, 'r') as f:
|
|
48
|
+
self.results[key] = json.load(f)
|
|
49
|
+
except Exception as e:
|
|
50
|
+
self.results[key] = {'error': str(e)}
|
|
51
|
+
else:
|
|
52
|
+
self.results[key] = {}
|
|
53
|
+
|
|
54
|
+
def generate_main_report(self, output_file):
|
|
55
|
+
"""Generate main index report with links to detailed reports"""
|
|
56
|
+
|
|
57
|
+
md = []
|
|
58
|
+
|
|
59
|
+
# Header
|
|
60
|
+
md.append("# Android Malware Analysis Report\n")
|
|
61
|
+
md.append(f"**Generated:** {self.timestamp}\n")
|
|
62
|
+
md.append(f"**APK:** `{self.apk_name}`\n")
|
|
63
|
+
md.append(f"**SHA256:** `{self.sha256}`\n")
|
|
64
|
+
md.append("\n---\n")
|
|
65
|
+
|
|
66
|
+
# Quick Navigation
|
|
67
|
+
md.append("\n## 📑 Quick Navigation\n")
|
|
68
|
+
md.append("- [Executive Summary](#executive-summary)")
|
|
69
|
+
md.append("- [APK Information](#apk-information)")
|
|
70
|
+
md.append("- [Protection & Obfuscation](#protection-obfuscation)")
|
|
71
|
+
md.append("- 🔗 [Security Findings (SAST)](./sast_findings.md)")
|
|
72
|
+
md.append("- 🔗 [AndroidManifest Analysis](./manifest_analysis.md)")
|
|
73
|
+
md.append("- 🔗 [Network Analysis](./network_analysis.md)")
|
|
74
|
+
md.append("- 🔗 [Entropy Analysis](./entropy_analysis.md) - **Encrypted Payload Detection**")
|
|
75
|
+
md.append("- 🔗 [YARA Scan Results](./yara_results.md)")
|
|
76
|
+
md.append("- 🔗 [Detailed Recommendations](./recommendations.md)")
|
|
77
|
+
md.append("")
|
|
78
|
+
|
|
79
|
+
# Executive Summary
|
|
80
|
+
md.append("\n## 📊 Executive Summary {#executive-summary}\n")
|
|
81
|
+
self._add_executive_summary(md)
|
|
82
|
+
|
|
83
|
+
# APK Information
|
|
84
|
+
md.append("\n## 📱 APK Information {#apk-information}\n")
|
|
85
|
+
self._add_apk_info(md)
|
|
86
|
+
|
|
87
|
+
# Protection & Obfuscation
|
|
88
|
+
md.append("\n## 🔒 Protection & Obfuscation Analysis {#protection-obfuscation}\n")
|
|
89
|
+
self._add_obfuscation_info(md)
|
|
90
|
+
|
|
91
|
+
# Summary sections with links to details
|
|
92
|
+
md.append("\n## 📊 Analysis Results Summary\n")
|
|
93
|
+
md.append(self._get_findings_summary())
|
|
94
|
+
|
|
95
|
+
md.append("\n### For Detailed Analysis:\n")
|
|
96
|
+
md.append("- 📄 **[Security Findings (SAST)](./sast_findings.md)** - Complete vulnerability analysis")
|
|
97
|
+
md.append("- 📄 **[Manifest Analysis](./manifest_analysis.md)** - Permission and component security")
|
|
98
|
+
md.append("- 📄 **[Network Analysis](./network_analysis.md)** - Network artifacts and C2 indicators")
|
|
99
|
+
md.append("- 📄 **[Entropy Analysis](./entropy_analysis.md)** - Encrypted payload detection & visualization")
|
|
100
|
+
md.append("- 📄 **[YARA Results](./yara_results.md)** - Malware pattern matching with detailed categorization")
|
|
101
|
+
md.append("- 📄 **[Recommendations](./recommendations.md)** - Actionable security guidance")
|
|
102
|
+
md.append("")
|
|
103
|
+
|
|
104
|
+
# Write main report
|
|
105
|
+
with open(output_file, 'w') as f:
|
|
106
|
+
f.write('\n'.join(md))
|
|
107
|
+
|
|
108
|
+
# Generate detailed reports
|
|
109
|
+
self._generate_sast_report()
|
|
110
|
+
self._generate_manifest_report()
|
|
111
|
+
self._generate_network_report()
|
|
112
|
+
self._generate_entropy_report()
|
|
113
|
+
self._generate_yara_report()
|
|
114
|
+
self._generate_recommendations_report()
|
|
115
|
+
|
|
116
|
+
def _get_findings_summary(self):
|
|
117
|
+
"""Get summary of findings"""
|
|
118
|
+
lines = []
|
|
119
|
+
|
|
120
|
+
# SAST Summary
|
|
121
|
+
sast = self.results.get('sast', {}).get('summary', {})
|
|
122
|
+
if sast:
|
|
123
|
+
lines.append(f"\n**🔴 Security Findings:** {sast.get('total_findings', 0)} issues found")
|
|
124
|
+
lines.append(f"- Critical: {sast.get('critical', 0)}")
|
|
125
|
+
lines.append(f"- High: {sast.get('high', 0)}")
|
|
126
|
+
lines.append(f"- Medium: {sast.get('medium', 0)}")
|
|
127
|
+
lines.append(f"- Low: {sast.get('low', 0)}")
|
|
128
|
+
|
|
129
|
+
# Manifest Summary
|
|
130
|
+
manifest = self.results.get('manifest', {})
|
|
131
|
+
if manifest:
|
|
132
|
+
dangerous_perms = len(manifest.get('dangerous_permissions', []))
|
|
133
|
+
exported_components = len(manifest.get('exported_components', []))
|
|
134
|
+
lines.append(f"\n**📋 Manifest Issues:**")
|
|
135
|
+
lines.append(f"- Dangerous permissions: {dangerous_perms}")
|
|
136
|
+
lines.append(f"- Exported components: {exported_components}")
|
|
137
|
+
|
|
138
|
+
# Network Summary
|
|
139
|
+
network = self.results.get('network', {})
|
|
140
|
+
if network:
|
|
141
|
+
urls = len(network.get('urls', []))
|
|
142
|
+
suspicious = len(network.get('suspicious_urls', []))
|
|
143
|
+
lines.append(f"\n**🌐 Network Artifacts:**")
|
|
144
|
+
lines.append(f"- URLs found: {urls}")
|
|
145
|
+
lines.append(f"- Suspicious URLs: {suspicious}")
|
|
146
|
+
|
|
147
|
+
# Entropy Summary
|
|
148
|
+
entropy = self.results.get('entropy', {})
|
|
149
|
+
if entropy and 'summary' in entropy:
|
|
150
|
+
summary = entropy['summary']
|
|
151
|
+
lines.append(f"\n**📊 Entropy Analysis:**")
|
|
152
|
+
lines.append(f"- Highly suspicious files: {len(summary.get('highly_suspicious', []))}")
|
|
153
|
+
lines.append(f"- Suspicious files: {len(summary.get('suspicious', []))}")
|
|
154
|
+
|
|
155
|
+
# YARA Summary
|
|
156
|
+
yara = self.results.get('yara', {})
|
|
157
|
+
if yara and 'statistics' in yara:
|
|
158
|
+
stats = yara['statistics']
|
|
159
|
+
lines.append(f"\n**🎯 YARA Matches:**")
|
|
160
|
+
lines.append(f"- Total matches: {stats.get('total_matches', 0)}")
|
|
161
|
+
by_sev = stats.get('by_severity', {})
|
|
162
|
+
if by_sev.get('CRITICAL', 0) > 0:
|
|
163
|
+
lines.append(f"- 🔴 Critical: {by_sev['CRITICAL']}")
|
|
164
|
+
if by_sev.get('HIGH', 0) > 0:
|
|
165
|
+
lines.append(f"- 🟠 High: {by_sev['HIGH']}")
|
|
166
|
+
|
|
167
|
+
return '\n'.join(lines)
|
|
168
|
+
|
|
169
|
+
def _generate_sast_report(self):
|
|
170
|
+
"""Generate detailed SAST findings report"""
|
|
171
|
+
output_file = self.report_dir / 'sast_findings.md'
|
|
172
|
+
|
|
173
|
+
md = []
|
|
174
|
+
md.append("# 🚨 Security Findings (SAST)\n")
|
|
175
|
+
md.append(f"**Analysis Date:** {self.timestamp}\n")
|
|
176
|
+
md.append(f"**APK:** {self.apk_name}\n")
|
|
177
|
+
md.append("\n[← Back to Main Report](./report.md)\n")
|
|
178
|
+
md.append("\n---\n")
|
|
179
|
+
|
|
180
|
+
# Add SAST findings
|
|
181
|
+
self._add_sast_findings(md)
|
|
182
|
+
|
|
183
|
+
with open(output_file, 'w') as f:
|
|
184
|
+
f.write('\n'.join(md))
|
|
185
|
+
|
|
186
|
+
def _generate_manifest_report(self):
|
|
187
|
+
"""Generate detailed manifest analysis report"""
|
|
188
|
+
output_file = self.report_dir / 'manifest_analysis.md'
|
|
189
|
+
|
|
190
|
+
md = []
|
|
191
|
+
md.append("# 📋 AndroidManifest Analysis\n")
|
|
192
|
+
md.append(f"**Analysis Date:** {self.timestamp}\n")
|
|
193
|
+
md.append(f"**APK:** {self.apk_name}\n")
|
|
194
|
+
md.append("\n[← Back to Main Report](./report.md)\n")
|
|
195
|
+
md.append("\n---\n")
|
|
196
|
+
|
|
197
|
+
# Add manifest findings
|
|
198
|
+
self._add_manifest_findings(md)
|
|
199
|
+
|
|
200
|
+
with open(output_file, 'w') as f:
|
|
201
|
+
f.write('\n'.join(md))
|
|
202
|
+
|
|
203
|
+
def _generate_network_report(self):
|
|
204
|
+
"""Generate detailed network analysis report"""
|
|
205
|
+
output_file = self.report_dir / 'network_analysis.md'
|
|
206
|
+
|
|
207
|
+
md = []
|
|
208
|
+
md.append("# 🌐 Network Analysis\n")
|
|
209
|
+
md.append(f"**Analysis Date:** {self.timestamp}\n")
|
|
210
|
+
md.append(f"**APK:** {self.apk_name}\n")
|
|
211
|
+
md.append("\n[← Back to Main Report](./report.md)\n")
|
|
212
|
+
md.append("\n---\n")
|
|
213
|
+
|
|
214
|
+
# Add network findings
|
|
215
|
+
self._add_network_findings(md)
|
|
216
|
+
|
|
217
|
+
with open(output_file, 'w') as f:
|
|
218
|
+
f.write('\n'.join(md))
|
|
219
|
+
|
|
220
|
+
def _generate_entropy_report(self):
|
|
221
|
+
"""Generate detailed entropy analysis report"""
|
|
222
|
+
output_file = self.report_dir / 'entropy_analysis.md'
|
|
223
|
+
|
|
224
|
+
md = []
|
|
225
|
+
md.append("# 📊 Entropy Analysis - Encrypted Payload Detection\n")
|
|
226
|
+
md.append(f"**Analysis Date:** {self.timestamp}\n")
|
|
227
|
+
md.append(f"**APK:** {self.apk_name}\n")
|
|
228
|
+
md.append("\n[← Back to Main Report](./report.md)\n")
|
|
229
|
+
md.append("\n---\n")
|
|
230
|
+
|
|
231
|
+
# Add entropy findings
|
|
232
|
+
self._add_entropy_findings(md)
|
|
233
|
+
|
|
234
|
+
with open(output_file, 'w') as f:
|
|
235
|
+
f.write('\n'.join(md))
|
|
236
|
+
|
|
237
|
+
def _generate_yara_report(self):
|
|
238
|
+
"""Generate detailed YARA results report"""
|
|
239
|
+
output_file = self.report_dir / 'yara_results.md'
|
|
240
|
+
|
|
241
|
+
md = []
|
|
242
|
+
md.append("# 🎯 YARA Scan Results\n")
|
|
243
|
+
md.append(f"**Analysis Date:** {self.timestamp}\n")
|
|
244
|
+
md.append(f"**APK:** {self.apk_name}\n")
|
|
245
|
+
md.append("\n[← Back to Main Report](./report.md)\n")
|
|
246
|
+
md.append("\n---\n")
|
|
247
|
+
|
|
248
|
+
# Add YARA results
|
|
249
|
+
self._add_yara_results(md)
|
|
250
|
+
|
|
251
|
+
with open(output_file, 'w') as f:
|
|
252
|
+
f.write('\n'.join(md))
|
|
253
|
+
|
|
254
|
+
def _generate_recommendations_report(self):
|
|
255
|
+
"""Generate detailed recommendations report"""
|
|
256
|
+
output_file = self.report_dir / 'recommendations.md'
|
|
257
|
+
|
|
258
|
+
md = []
|
|
259
|
+
md.append("# 💡 Security Recommendations\n")
|
|
260
|
+
md.append(f"**Analysis Date:** {self.timestamp}\n")
|
|
261
|
+
md.append(f"**APK:** {self.apk_name}\n")
|
|
262
|
+
md.append("\n[← Back to Main Report](./report.md)\n")
|
|
263
|
+
md.append("\n---\n")
|
|
264
|
+
|
|
265
|
+
# Add recommendations
|
|
266
|
+
self._add_recommendations(md)
|
|
267
|
+
|
|
268
|
+
with open(output_file, 'w') as f:
|
|
269
|
+
f.write('\n'.join(md))
|
|
270
|
+
|
|
271
|
+
def _add_executive_summary(self, md):
|
|
272
|
+
"""Add executive summary"""
|
|
273
|
+
|
|
274
|
+
# Calculate risk level
|
|
275
|
+
risk_score = self.results.get('sast', {}).get('summary', {}).get('risk_score', 0)
|
|
276
|
+
|
|
277
|
+
if risk_score >= 80:
|
|
278
|
+
risk_level = "🔴 **CRITICAL**"
|
|
279
|
+
elif risk_score >= 60:
|
|
280
|
+
risk_level = "🟠 **HIGH**"
|
|
281
|
+
elif risk_score >= 40:
|
|
282
|
+
risk_level = "🟡 **MEDIUM**"
|
|
283
|
+
elif risk_score >= 20:
|
|
284
|
+
risk_level = "🟢 **LOW**"
|
|
285
|
+
else:
|
|
286
|
+
risk_level = "⚪ **MINIMAL**"
|
|
287
|
+
|
|
288
|
+
md.append(f"**Overall Risk Level:** {risk_level} (Score: {risk_score}/100)\n")
|
|
289
|
+
|
|
290
|
+
obf_type = self.results.get('obfuscation', {}).get('type', 'none')
|
|
291
|
+
md.append(f"- **Obfuscation Type:** `{obf_type.upper()}`")
|
|
292
|
+
|
|
293
|
+
total_findings = self.results.get('sast', {}).get('summary', {}).get('total_findings', 0)
|
|
294
|
+
critical_findings = self.results.get('sast', {}).get('summary', {}).get('critical', 0)
|
|
295
|
+
md.append(f"- **Total Security Findings:** {total_findings}")
|
|
296
|
+
md.append(f"- **Critical Issues:** {critical_findings}")
|
|
297
|
+
md.append("")
|
|
298
|
+
|
|
299
|
+
def _add_apk_info(self, md):
|
|
300
|
+
"""Add APK information"""
|
|
301
|
+
basic_info = self.results.get('basic_info', {})
|
|
302
|
+
|
|
303
|
+
md.append("| Property | Value |")
|
|
304
|
+
md.append("|----------|-------|")
|
|
305
|
+
md.append(f"| Package Name | `{basic_info.get('package_name', 'N/A')}` |")
|
|
306
|
+
md.append(f"| App Name | {basic_info.get('app_name', 'N/A')} |")
|
|
307
|
+
md.append(f"| Version | {basic_info.get('version_name', 'N/A')} ({basic_info.get('version_code', 'N/A')}) |")
|
|
308
|
+
md.append(f"| Min SDK | {basic_info.get('min_sdk', 'N/A')} |")
|
|
309
|
+
md.append(f"| Target SDK | {basic_info.get('target_sdk', 'N/A')} |")
|
|
310
|
+
md.append(f"| Signed | {basic_info.get('is_signed', False)} |")
|
|
311
|
+
md.append(f"| Permissions | {len(basic_info.get('permissions', []))} |")
|
|
312
|
+
md.append(f"| Activities | {len(basic_info.get('activities', []))} |")
|
|
313
|
+
md.append(f"| Services | {len(basic_info.get('services', []))} |")
|
|
314
|
+
md.append(f"| Receivers | {len(basic_info.get('receivers', []))} |")
|
|
315
|
+
md.append("")
|
|
316
|
+
|
|
317
|
+
def _add_obfuscation_info(self, md):
|
|
318
|
+
"""Add obfuscation detection info"""
|
|
319
|
+
obf = self.results.get('obfuscation', {})
|
|
320
|
+
dex_payload = self.results.get('dex_payload', {})
|
|
321
|
+
so_strings = self.results.get('so_strings', [])
|
|
322
|
+
|
|
323
|
+
obf_type = obf.get('type', 'none')
|
|
324
|
+
confidence = obf.get('confidence', 0)
|
|
325
|
+
|
|
326
|
+
md.append(f"**Type:** `{obf_type.upper()}`")
|
|
327
|
+
md.append(f"**Confidence:** {confidence}%\n")
|
|
328
|
+
|
|
329
|
+
# DEX Payload Analysis
|
|
330
|
+
if dex_payload:
|
|
331
|
+
stub_detected = dex_payload.get('stub_dex_detected', False)
|
|
332
|
+
encrypted_payloads = dex_payload.get('encrypted_payloads', [])
|
|
333
|
+
|
|
334
|
+
if stub_detected:
|
|
335
|
+
md.append("### 🚨 Stub DEX Detected\n")
|
|
336
|
+
md.append("⚠️ **Real DEX is NOT in the APK** - it will be unpacked at runtime\n")
|
|
337
|
+
|
|
338
|
+
for indicator in dex_payload.get('protection_indicators', []):
|
|
339
|
+
md.append(f"**{indicator.get('file')}:** {indicator.get('type')}")
|
|
340
|
+
for ind in indicator.get('indicators', []):
|
|
341
|
+
md.append(f"- {ind}")
|
|
342
|
+
md.append("")
|
|
343
|
+
|
|
344
|
+
# Protection Library String Analysis
|
|
345
|
+
if so_strings and len(so_strings) > 0:
|
|
346
|
+
md.append("### 🔍 Protection Library Analysis\n")
|
|
347
|
+
md.append("**Native libraries analyzed for protection artifacts:**\n")
|
|
348
|
+
|
|
349
|
+
for lib_result in so_strings:
|
|
350
|
+
lib_file = lib_result.get('file', 'Unknown')
|
|
351
|
+
stats = lib_result.get('stats', {})
|
|
352
|
+
categories = lib_result.get('categories', {})
|
|
353
|
+
|
|
354
|
+
md.append(f"\n#### 📦 `{lib_file}`\n")
|
|
355
|
+
md.append(f"- **Size:** {stats.get('file_size_kb', 0):.2f} KB")
|
|
356
|
+
md.append(f"- **Strings Extracted:** {stats.get('total_strings', 0)}")
|
|
357
|
+
md.append(f"- **Categories Found:** {stats.get('categories_found', 0)}\n")
|
|
358
|
+
|
|
359
|
+
# Show critical findings
|
|
360
|
+
critical_cats = {
|
|
361
|
+
'dex_files': '🎯 DEX Files',
|
|
362
|
+
'zip_files': '🔒 ZIP Files (Encrypted Containers)',
|
|
363
|
+
'code_cache_paths': '📁 Code Cache Paths',
|
|
364
|
+
'encryption_keywords': '🔐 Encryption Keywords',
|
|
365
|
+
'class_loaders': '🔧 ClassLoaders',
|
|
366
|
+
'shell_keywords': '🐚 Shell/Protection Keywords'
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
for cat_key, cat_name in critical_cats.items():
|
|
370
|
+
if cat_key in categories:
|
|
371
|
+
items = categories[cat_key]
|
|
372
|
+
md.append(f"**{cat_name}:** {len(items)} found")
|
|
373
|
+
for item in items[:5]: # Show first 5
|
|
374
|
+
md.append(f" - `{item}`")
|
|
375
|
+
if len(items) > 5:
|
|
376
|
+
md.append(f" - ... and {len(items) - 5} more")
|
|
377
|
+
md.append("")
|
|
378
|
+
md.append("")
|
|
379
|
+
|
|
380
|
+
if encrypted_payloads:
|
|
381
|
+
md.append(f"### 🔒 Encrypted Payloads Found: {len(encrypted_payloads)}\n")
|
|
382
|
+
for payload in encrypted_payloads[:5]: # Show first 5
|
|
383
|
+
md.append(f"**{payload['location']}**")
|
|
384
|
+
md.append(f"- Type: {payload['type']}")
|
|
385
|
+
md.append(f"- Size: {payload['size_kb']:.1f} KB")
|
|
386
|
+
md.append(f"- Entropy: {payload['entropy']}")
|
|
387
|
+
for ind in payload.get('indicators', []):
|
|
388
|
+
md.append(f"- {ind}")
|
|
389
|
+
md.append("")
|
|
390
|
+
|
|
391
|
+
if len(encrypted_payloads) > 5:
|
|
392
|
+
md.append(f"*...and {len(encrypted_payloads) - 5} more encrypted payloads*\n")
|
|
393
|
+
|
|
394
|
+
# Likely unpack locations
|
|
395
|
+
unpack_locs = dex_payload.get('likely_unpack_locations', [])
|
|
396
|
+
if unpack_locs and (stub_detected or encrypted_payloads):
|
|
397
|
+
md.append("### 📍 Predicted Runtime Unpack Locations\n")
|
|
398
|
+
md.append("Monitor these paths during dynamic analysis:\n")
|
|
399
|
+
for loc in unpack_locs[:5]:
|
|
400
|
+
if loc['priority'] == 'HIGH':
|
|
401
|
+
md.append(f"- 🔴 `{loc['path']}`")
|
|
402
|
+
md.append(f" {loc['description']}")
|
|
403
|
+
md.append("")
|
|
404
|
+
|
|
405
|
+
indicators = obf.get('indicators', [])
|
|
406
|
+
if indicators:
|
|
407
|
+
md.append("### Detection Indicators\n")
|
|
408
|
+
for ind in indicators[:10]: # Show first 10
|
|
409
|
+
md.append(f"- {ind}")
|
|
410
|
+
if len(indicators) > 10:
|
|
411
|
+
md.append(f"\n*...and {len(indicators) - 10} more indicators*")
|
|
412
|
+
md.append("")
|
|
413
|
+
|
|
414
|
+
# APKiD results if available
|
|
415
|
+
apkid = obf.get('apkid_results', {})
|
|
416
|
+
if apkid:
|
|
417
|
+
md.append("### APKiD Enhanced Detection\n")
|
|
418
|
+
md.append("✅ APKiD analysis completed\n")
|
|
419
|
+
|
|
420
|
+
md.append("### Analysis Recommendations\n")
|
|
421
|
+
|
|
422
|
+
# DEX payload recommendations
|
|
423
|
+
if dex_payload:
|
|
424
|
+
for rec in dex_payload.get('recommendations', [])[:3]:
|
|
425
|
+
priority_icon = '🔴' if rec.get('priority') == 'HIGH' else '🟡'
|
|
426
|
+
md.append(f"{priority_icon} **{rec.get('action')}**")
|
|
427
|
+
md.append(f" {rec.get('details')} ")
|
|
428
|
+
md.append("")
|
|
429
|
+
|
|
430
|
+
# Obfuscation recommendations
|
|
431
|
+
recs = obf.get('recommendations', [])
|
|
432
|
+
for rec in recs:
|
|
433
|
+
md.append(f"{rec} ")
|
|
434
|
+
md.append("")
|
|
435
|
+
|
|
436
|
+
def _add_sast_findings(self, md):
|
|
437
|
+
"""Add SAST findings (full details)"""
|
|
438
|
+
sast = self.results.get('sast', {})
|
|
439
|
+
summary = sast.get('summary', {})
|
|
440
|
+
|
|
441
|
+
md.append("### Summary\n")
|
|
442
|
+
md.append("| Severity | Count |")
|
|
443
|
+
md.append("|----------|-------|")
|
|
444
|
+
total_findings = 0
|
|
445
|
+
for severity in ['critical', 'high', 'medium', 'low']:
|
|
446
|
+
count = summary.get('by_severity', {}).get(severity, 0)
|
|
447
|
+
total_findings += count
|
|
448
|
+
emoji = {'critical': '🔴', 'high': '🟠', 'medium': '🟡', 'low': '🔵'}
|
|
449
|
+
md.append(f"| {emoji[severity]} {severity.title()} | {count} |")
|
|
450
|
+
md.append("")
|
|
451
|
+
|
|
452
|
+
md.append(f"**Total Findings:** {total_findings}\n")
|
|
453
|
+
|
|
454
|
+
if total_findings == 0:
|
|
455
|
+
md.append("✅ No security issues found\n")
|
|
456
|
+
return
|
|
457
|
+
|
|
458
|
+
# Group findings by severity - read directly from arrays
|
|
459
|
+
for severity in ['critical', 'high', 'medium', 'low']:
|
|
460
|
+
findings = sast.get(severity, [])
|
|
461
|
+
if not findings:
|
|
462
|
+
continue
|
|
463
|
+
|
|
464
|
+
icons = {'critical': '🔴', 'high': '🟠', 'medium': '🟡', 'low': '🔵'}
|
|
465
|
+
md.append(f"\n## {icons[severity]} {severity.upper()} Severity Findings\n")
|
|
466
|
+
|
|
467
|
+
# Group by category
|
|
468
|
+
by_category = {}
|
|
469
|
+
for finding in findings:
|
|
470
|
+
cat = finding.get('category', 'other')
|
|
471
|
+
if cat not in by_category:
|
|
472
|
+
by_category[cat] = []
|
|
473
|
+
by_category[cat].append(finding)
|
|
474
|
+
|
|
475
|
+
for category, cat_findings in by_category.items():
|
|
476
|
+
md.append(f"\n### {category.replace('_', ' ').title()} ({len(cat_findings)} issues)\n")
|
|
477
|
+
|
|
478
|
+
for i, finding in enumerate(cat_findings, 1):
|
|
479
|
+
md.append(f"**{i}. {finding.get('title', 'Security Issue')}**")
|
|
480
|
+
md.append(f"- **Description:** {finding.get('description', 'N/A')}")
|
|
481
|
+
md.append(f"- **File:** `{finding.get('file', 'N/A')}`")
|
|
482
|
+
if finding.get('line'):
|
|
483
|
+
md.append(f"- **Line:** {finding['line']}")
|
|
484
|
+
md.append("")
|
|
485
|
+
|
|
486
|
+
code = finding.get('code', '')
|
|
487
|
+
if code:
|
|
488
|
+
md.append("```java")
|
|
489
|
+
md.append(code)
|
|
490
|
+
md.append("```\n")
|
|
491
|
+
|
|
492
|
+
def _add_manifest_findings(self, md):
|
|
493
|
+
"""Add manifest analysis findings"""
|
|
494
|
+
manifest = self.results.get('manifest', {})
|
|
495
|
+
findings = manifest.get('findings', [])
|
|
496
|
+
|
|
497
|
+
if not findings:
|
|
498
|
+
md.append("✅ No significant issues found in AndroidManifest.xml\n")
|
|
499
|
+
return
|
|
500
|
+
|
|
501
|
+
md.append(f"**Total Issues:** {len(findings)}\n")
|
|
502
|
+
|
|
503
|
+
# Group by severity
|
|
504
|
+
by_severity = {'critical': [], 'high': [], 'medium': [], 'low': []}
|
|
505
|
+
for finding in findings:
|
|
506
|
+
sev = finding.get('severity', 'low')
|
|
507
|
+
by_severity[sev].append(finding)
|
|
508
|
+
|
|
509
|
+
for severity in ['critical', 'high', 'medium']:
|
|
510
|
+
if not by_severity[severity]:
|
|
511
|
+
continue
|
|
512
|
+
|
|
513
|
+
emoji = {'critical': '🔴', 'high': '🟠', 'medium': '🟡'}
|
|
514
|
+
md.append(f"\n## {emoji[severity]} {severity.title()} Issues ({len(by_severity[severity])})\n")
|
|
515
|
+
|
|
516
|
+
for i, finding in enumerate(by_severity[severity], 1):
|
|
517
|
+
md.append(f"**{i}. {finding.get('title', 'Unknown')}**")
|
|
518
|
+
md.append(f"- **Description:** {finding.get('description', 'N/A')}")
|
|
519
|
+
if finding.get('permission'):
|
|
520
|
+
md.append(f"- **Permission:** `{finding['permission']}`")
|
|
521
|
+
if finding.get('component'):
|
|
522
|
+
md.append(f"- **Component:** `{finding['component']}`")
|
|
523
|
+
md.append("")
|
|
524
|
+
|
|
525
|
+
def _add_network_findings(self, md):
|
|
526
|
+
"""Add network analysis findings"""
|
|
527
|
+
network = self.results.get('network', {})
|
|
528
|
+
|
|
529
|
+
urls = network.get('urls', [])
|
|
530
|
+
domains = network.get('domains', [])
|
|
531
|
+
ips = network.get('ips', [])
|
|
532
|
+
suspicious_urls = network.get('suspicious_urls', [])
|
|
533
|
+
|
|
534
|
+
if suspicious_urls:
|
|
535
|
+
md.append("### 🚨 Suspicious URLs\n")
|
|
536
|
+
for url in suspicious_urls: # Show all
|
|
537
|
+
md.append(f"- `{url}`")
|
|
538
|
+
md.append("")
|
|
539
|
+
|
|
540
|
+
if urls:
|
|
541
|
+
md.append(f"### URLs Found ({len(urls)})\n")
|
|
542
|
+
for url in urls: # Show all
|
|
543
|
+
md.append(f"- `{url}`")
|
|
544
|
+
md.append("")
|
|
545
|
+
|
|
546
|
+
if domains:
|
|
547
|
+
md.append(f"### Unique Domains ({len(domains)})\n")
|
|
548
|
+
for domain in list(domains): # Show all
|
|
549
|
+
md.append(f"- `{domain}`")
|
|
550
|
+
md.append("")
|
|
551
|
+
|
|
552
|
+
if ips:
|
|
553
|
+
md.append(f"### IP Addresses ({len(ips)})\n")
|
|
554
|
+
for ip in ips:
|
|
555
|
+
md.append(f"- `{ip}`")
|
|
556
|
+
md.append("")
|
|
557
|
+
|
|
558
|
+
def _add_entropy_findings(self, md):
|
|
559
|
+
"""Add entropy analysis findings"""
|
|
560
|
+
entropy = self.results.get('entropy', {})
|
|
561
|
+
|
|
562
|
+
if not entropy or 'files' not in entropy:
|
|
563
|
+
md.append("⚠️ Entropy analysis data not available\n")
|
|
564
|
+
return
|
|
565
|
+
|
|
566
|
+
summary = entropy.get('summary', {})
|
|
567
|
+
files = entropy.get('files', [])
|
|
568
|
+
|
|
569
|
+
md.append("## Overview\n")
|
|
570
|
+
md.append("Entropy analysis identifies encrypted or heavily obfuscated regions in APK files.")
|
|
571
|
+
md.append("High entropy (>7.0) typically indicates encryption, while low entropy (<5.0) suggests plaintext.\n")
|
|
572
|
+
md.append(f"**Files Analyzed:** {summary.get('total_files', len(files))}\n")
|
|
573
|
+
|
|
574
|
+
# Highly suspicious files
|
|
575
|
+
highly_suspicious = summary.get('highly_suspicious', [])
|
|
576
|
+
if highly_suspicious:
|
|
577
|
+
md.append(f"\n## 🔴 Highly Suspicious Files ({len(highly_suspicious)})\n")
|
|
578
|
+
md.append("These files show characteristics of strong encryption - likely encrypted payloads.\n")
|
|
579
|
+
|
|
580
|
+
for file_entry in highly_suspicious:
|
|
581
|
+
filename = file_entry['filename']
|
|
582
|
+
entropy_val = file_entry['entropy']
|
|
583
|
+
ratio = file_entry['high_entropy_ratio']
|
|
584
|
+
|
|
585
|
+
md.append(f"\n### `{filename}`\n")
|
|
586
|
+
md.append(f"- **Overall Entropy:** {entropy_val:.2f}/8.0")
|
|
587
|
+
md.append(f"- **High Entropy Ratio:** {ratio:.1%}")
|
|
588
|
+
md.append(f"- **Assessment:** Likely encrypted payload - dynamic analysis required\n")
|
|
589
|
+
|
|
590
|
+
# Find and display chart if available
|
|
591
|
+
file_data = next((f for f in files if f.get('filename') == filename), None)
|
|
592
|
+
if file_data and 'chart' in file_data:
|
|
593
|
+
md.append("**Entropy Distribution:**\n")
|
|
594
|
+
md.append("```")
|
|
595
|
+
md.append(file_data['chart'])
|
|
596
|
+
md.append("```\n")
|
|
597
|
+
|
|
598
|
+
if file_data and 'assessment' in file_data:
|
|
599
|
+
md.append(f"**Recommendation:** {file_data['assessment'].get('recommendation', 'N/A')}\n")
|
|
600
|
+
|
|
601
|
+
# Suspicious files
|
|
602
|
+
suspicious = summary.get('suspicious', [])
|
|
603
|
+
if suspicious:
|
|
604
|
+
md.append(f"\n## 🟡 Suspicious Files ({len(suspicious)})\n")
|
|
605
|
+
md.append("These files contain elevated entropy regions - may contain compressed or partially encrypted data.\n")
|
|
606
|
+
|
|
607
|
+
for file_entry in suspicious:
|
|
608
|
+
filename = file_entry['filename']
|
|
609
|
+
entropy_val = file_entry['entropy']
|
|
610
|
+
ratio = file_entry['high_entropy_ratio']
|
|
611
|
+
|
|
612
|
+
md.append(f"\n### `{filename}`\n")
|
|
613
|
+
md.append(f"- **Overall Entropy:** {entropy_val:.2f}/8.0")
|
|
614
|
+
md.append(f"- **High Entropy Ratio:** {ratio:.1%}\n")
|
|
615
|
+
|
|
616
|
+
# Normal files
|
|
617
|
+
normal = summary.get('normal', [])
|
|
618
|
+
if normal:
|
|
619
|
+
md.append(f"\n## 🟢 Normal Files ({len(normal)})\n")
|
|
620
|
+
md.append("These files show low entropy - mostly plaintext or uncompressed code.\n")
|
|
621
|
+
|
|
622
|
+
md.append("\n## Interpretation Guide\n")
|
|
623
|
+
md.append("| Entropy Range | Interpretation |")
|
|
624
|
+
md.append("|--------------|----------------|")
|
|
625
|
+
md.append("| 7.5 - 8.0 | Heavily encrypted - likely encrypted payload |")
|
|
626
|
+
md.append("| 7.0 - 7.5 | Strong encryption/compression detected |")
|
|
627
|
+
md.append("| 6.0 - 7.0 | Moderate obfuscation or compression |")
|
|
628
|
+
md.append("| 5.0 - 6.0 | Some obfuscation, mostly normal code |")
|
|
629
|
+
md.append("| 0.0 - 5.0 | Low entropy - plaintext or uncompressed |")
|
|
630
|
+
md.append("")
|
|
631
|
+
|
|
632
|
+
def _add_yara_results(self, md):
|
|
633
|
+
"""Add YARA scan results with detailed rule information"""
|
|
634
|
+
yara_data = self.results.get('yara', {})
|
|
635
|
+
|
|
636
|
+
# If no enhanced results, try to parse raw YARA files
|
|
637
|
+
if not yara_data or 'summary' not in yara_data:
|
|
638
|
+
md.append("## Overview\n")
|
|
639
|
+
md.append("⚠️ Enhanced YARA analysis not available. Showing raw results:\n")
|
|
640
|
+
self._add_raw_yara_results(md)
|
|
641
|
+
return
|
|
642
|
+
|
|
643
|
+
summary = yara_data.get('summary', {})
|
|
644
|
+
findings = yara_data.get('findings', [])
|
|
645
|
+
rules_checked = yara_data.get('rules_checked', [])
|
|
646
|
+
categories = yara_data.get('categories', {})
|
|
647
|
+
scan_details = yara_data.get('scan_details', {})
|
|
648
|
+
|
|
649
|
+
# Summary section
|
|
650
|
+
md.append("## Overview\n")
|
|
651
|
+
md.append(f"- **Rules Checked:** {summary.get('total_rules_checked', 0)}")
|
|
652
|
+
md.append(f"- **Rules Matched:** {summary.get('rules_matched', 0)}/{summary.get('total_rules_checked', 0)}")
|
|
653
|
+
md.append(f"- **Total Matches:** {summary.get('total_matches', 0)}")
|
|
654
|
+
md.append(f"- **Files Scanned:** {summary.get('files_scanned', 0)}\n")
|
|
655
|
+
|
|
656
|
+
# Scan coverage
|
|
657
|
+
md.append("### Scan Coverage\n")
|
|
658
|
+
md.append("| Scan Type | Files | Matches |")
|
|
659
|
+
md.append("|-----------|-------|---------|")
|
|
660
|
+
for scan_name, scan_info in scan_details.items():
|
|
661
|
+
scan_label = scan_name.replace('_scan', '').replace('_', ' ').title()
|
|
662
|
+
files = len(scan_info.get('files', []))
|
|
663
|
+
matches = scan_info.get('matches', 0)
|
|
664
|
+
md.append(f"| {scan_label} | {files} | {matches} |")
|
|
665
|
+
md.append("")
|
|
666
|
+
|
|
667
|
+
# Severity breakdown
|
|
668
|
+
md.append("### Severity Distribution\n")
|
|
669
|
+
md.append("| Severity | Count |")
|
|
670
|
+
md.append("|----------|-------|")
|
|
671
|
+
critical = summary.get('critical_findings', 0)
|
|
672
|
+
high = summary.get('high_severity', 0)
|
|
673
|
+
medium = summary.get('medium_severity', 0)
|
|
674
|
+
low = summary.get('low_severity', 0)
|
|
675
|
+
|
|
676
|
+
if critical > 0:
|
|
677
|
+
md.append(f"| 🔴 CRITICAL | {critical} |")
|
|
678
|
+
if high > 0:
|
|
679
|
+
md.append(f"| 🟠 HIGH | {high} |")
|
|
680
|
+
if medium > 0:
|
|
681
|
+
md.append(f"| 🟡 MEDIUM | {medium} |")
|
|
682
|
+
if low > 0:
|
|
683
|
+
md.append(f"| 🔵 LOW | {low} |")
|
|
684
|
+
md.append("")
|
|
685
|
+
|
|
686
|
+
# Rules checked table
|
|
687
|
+
if rules_checked:
|
|
688
|
+
md.append("\n## All Rules Checked\n")
|
|
689
|
+
md.append("Complete list of YARA rules that were executed during the scan.\n")
|
|
690
|
+
md.append("| Rule Name | Category | Severity | Status | Matches |")
|
|
691
|
+
md.append("|-----------|----------|----------|--------|---------|")
|
|
692
|
+
|
|
693
|
+
for rule in rules_checked:
|
|
694
|
+
name = rule['name']
|
|
695
|
+
category = rule['category']
|
|
696
|
+
severity = rule['severity']
|
|
697
|
+
matched = "✓ Matched" if rule['matched'] else "✗ No Match"
|
|
698
|
+
match_count = rule['match_count']
|
|
699
|
+
|
|
700
|
+
# Add emoji based on status
|
|
701
|
+
emoji = "🔍"
|
|
702
|
+
if rule['matched']:
|
|
703
|
+
if severity == 'CRITICAL':
|
|
704
|
+
emoji = "🔴"
|
|
705
|
+
elif severity == 'HIGH':
|
|
706
|
+
emoji = "🟠"
|
|
707
|
+
elif severity == 'MEDIUM':
|
|
708
|
+
emoji = "🟡"
|
|
709
|
+
else:
|
|
710
|
+
emoji = "🔵"
|
|
711
|
+
|
|
712
|
+
md.append(f"| {emoji} `{name}` | {category} | {severity} | {matched} | {match_count} |")
|
|
713
|
+
md.append("")
|
|
714
|
+
|
|
715
|
+
# Detailed findings by severity
|
|
716
|
+
if findings:
|
|
717
|
+
md.append("\n## Detailed Findings\n")
|
|
718
|
+
|
|
719
|
+
for severity in ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']:
|
|
720
|
+
severity_findings = [f for f in findings if f['severity'] == severity]
|
|
721
|
+
if not severity_findings:
|
|
722
|
+
continue
|
|
723
|
+
|
|
724
|
+
emoji = {'CRITICAL': '🔴', 'HIGH': '🟠', 'MEDIUM': '🟡', 'LOW': '🔵'}
|
|
725
|
+
md.append(f"\n### {emoji[severity]} {severity} Severity ({len(severity_findings)} matches)\n")
|
|
726
|
+
|
|
727
|
+
# Group by rule
|
|
728
|
+
from collections import defaultdict
|
|
729
|
+
by_rule = defaultdict(list)
|
|
730
|
+
for finding in severity_findings:
|
|
731
|
+
by_rule[finding['rule_name']].append(finding)
|
|
732
|
+
|
|
733
|
+
for rule_name, rule_findings in by_rule.items():
|
|
734
|
+
md.append(f"\n#### `{rule_name}` ({len(rule_findings)} occurrences)\n")
|
|
735
|
+
|
|
736
|
+
# Rule details
|
|
737
|
+
first = rule_findings[0]
|
|
738
|
+
md.append(f"**Category:** {first['category']}")
|
|
739
|
+
md.append(f"**Description:** {first['description']}")
|
|
740
|
+
md.append(f"**Recommendation:** {first['recommendation']}\n")
|
|
741
|
+
|
|
742
|
+
md.append("**Affected Files:**\n")
|
|
743
|
+
for i, finding in enumerate(rule_findings, 1):
|
|
744
|
+
file_path = finding['file']
|
|
745
|
+
scan_type = finding['scan_type']
|
|
746
|
+
|
|
747
|
+
md.append(f"{i}. `{file_path}` _(from {scan_type} scan)_")
|
|
748
|
+
|
|
749
|
+
# Show matched strings if available
|
|
750
|
+
matched_strings = finding.get('matched_strings', [])
|
|
751
|
+
if matched_strings:
|
|
752
|
+
md.append(f" - **Matched Patterns:** {len(matched_strings)}")
|
|
753
|
+
for j, string_match in enumerate(matched_strings[:3], 1):
|
|
754
|
+
offset = string_match['offset']
|
|
755
|
+
identifier = string_match['identifier']
|
|
756
|
+
content = string_match['content'][:60]
|
|
757
|
+
md.append(f" {j}. `{identifier}` at {offset}: `{content}...`")
|
|
758
|
+
if len(matched_strings) > 3:
|
|
759
|
+
md.append(f" ... and {len(matched_strings) - 3} more")
|
|
760
|
+
md.append("")
|
|
761
|
+
|
|
762
|
+
md.append("---\n")
|
|
763
|
+
|
|
764
|
+
def _add_raw_yara_results(self, md):
|
|
765
|
+
"""Fallback method to display raw YARA scan results"""
|
|
766
|
+
yara_files = [
|
|
767
|
+
('yara_apk_results.txt', 'APK Scan'),
|
|
768
|
+
('yara_decompiled_results.txt', 'Decompiled Files'),
|
|
769
|
+
('yara_jadx_results.txt', 'JADX Sources'),
|
|
770
|
+
]
|
|
771
|
+
|
|
772
|
+
has_results = False
|
|
773
|
+
for filename, title in yara_files:
|
|
774
|
+
filepath = self.temp_dir / filename
|
|
775
|
+
if filepath.exists():
|
|
776
|
+
try:
|
|
777
|
+
content = filepath.read_text(errors='ignore')
|
|
778
|
+
if content.strip():
|
|
779
|
+
has_results = True
|
|
780
|
+
md.append(f"\n### {title}\n")
|
|
781
|
+
md.append("```")
|
|
782
|
+
lines = content.strip().split('\n')
|
|
783
|
+
for line in lines[:100]: # Limit to 100 lines per file
|
|
784
|
+
md.append(line)
|
|
785
|
+
if len(lines) > 100:
|
|
786
|
+
md.append(f"\n... ({len(lines) - 100} more lines)")
|
|
787
|
+
md.append("```\n")
|
|
788
|
+
except Exception as e:
|
|
789
|
+
md.append(f"Error reading {filename}: {e}\n")
|
|
790
|
+
|
|
791
|
+
if not has_results:
|
|
792
|
+
md.append("✅ No YARA matches found\n")
|
|
793
|
+
|
|
794
|
+
def _add_recommendations(self, md):
|
|
795
|
+
"""Add security recommendations"""
|
|
796
|
+
|
|
797
|
+
obf = self.results.get('obfuscation', {})
|
|
798
|
+
sast = self.results.get('sast', {})
|
|
799
|
+
entropy = self.results.get('entropy', {})
|
|
800
|
+
yara = self.results.get('yara', {})
|
|
801
|
+
|
|
802
|
+
md.append("### Based on Protection Type\n")
|
|
803
|
+
obf_recs = obf.get('recommendations', [])
|
|
804
|
+
for rec in obf_recs:
|
|
805
|
+
md.append(f"{rec} ")
|
|
806
|
+
md.append("")
|
|
807
|
+
|
|
808
|
+
# Entropy-based recommendations
|
|
809
|
+
if entropy and entropy.get('summary', {}).get('highly_suspicious'):
|
|
810
|
+
md.append("### Based on Entropy Analysis\n")
|
|
811
|
+
md.append("🔴 **High entropy files detected:**")
|
|
812
|
+
md.append("1. These files are likely encrypted and cannot be analyzed statically")
|
|
813
|
+
md.append("2. **REQUIRED:** Dynamic analysis to capture unpacked DEX files")
|
|
814
|
+
md.append("3. Install APK on rooted device/emulator")
|
|
815
|
+
md.append("4. Use Frida scripts to dump DEX from memory at runtime")
|
|
816
|
+
md.append("5. Monitor file system for unpacked payloads\n")
|
|
817
|
+
|
|
818
|
+
md.append("### Based on SAST Findings\n")
|
|
819
|
+
critical_count = sast.get('summary', {}).get('critical', 0)
|
|
820
|
+
high_count = sast.get('summary', {}).get('high', 0)
|
|
821
|
+
|
|
822
|
+
if critical_count > 0:
|
|
823
|
+
md.append("1. **URGENT:** Address all critical severity findings immediately")
|
|
824
|
+
md.append("2. **PRIORITY:** Review and fix high severity issues")
|
|
825
|
+
elif high_count > 0:
|
|
826
|
+
md.append("1. **PRIORITY:** Review and fix high severity issues")
|
|
827
|
+
|
|
828
|
+
md.append("3. **Code Review:** Conduct thorough manual code review")
|
|
829
|
+
md.append("4. **Penetration Testing:** Perform dynamic analysis and penetration testing")
|
|
830
|
+
md.append("")
|
|
831
|
+
|
|
832
|
+
md.append("### General Security Recommendations\n")
|
|
833
|
+
md.append("1. **Dynamic Analysis:** Run the app in a controlled environment with Frida hooks")
|
|
834
|
+
md.append("2. **Network Monitoring:** Capture and analyze network traffic")
|
|
835
|
+
md.append("3. **Behavioral Analysis:** Monitor file system, IPC, and system calls")
|
|
836
|
+
md.append("4. **Memory Dump:** Extract and analyze runtime memory")
|
|
837
|
+
md.append("5. **API Hooking:** Hook sensitive APIs to understand behavior")
|
|
838
|
+
md.append("")
|
|
839
|
+
|
|
840
|
+
def generate_json(self, output_file):
|
|
841
|
+
"""Generate JSON report (complete data)"""
|
|
842
|
+
|
|
843
|
+
report_data = {
|
|
844
|
+
'metadata': {
|
|
845
|
+
'generated': self.timestamp,
|
|
846
|
+
'apk_name': self.apk_name,
|
|
847
|
+
'sha256': self.sha256,
|
|
848
|
+
},
|
|
849
|
+
'results': self.results
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
with open(output_file, 'w') as f:
|
|
853
|
+
json.dump(report_data, f, indent=2)
|
|
854
|
+
|
|
855
|
+
def main():
|
|
856
|
+
parser = argparse.ArgumentParser(description='Generate modular malware analysis reports')
|
|
857
|
+
parser.add_argument('workspace', help='Analysis workspace directory')
|
|
858
|
+
parser.add_argument('temp_dir', help='Temporary directory with analysis results')
|
|
859
|
+
parser.add_argument('apk_name', help='APK filename')
|
|
860
|
+
parser.add_argument('sha256', help='APK SHA256 hash')
|
|
861
|
+
parser.add_argument('--output-md', default='report.md', help='Main markdown report output')
|
|
862
|
+
parser.add_argument('--output-json', default='report.json', help='JSON report output')
|
|
863
|
+
|
|
864
|
+
args = parser.parse_args()
|
|
865
|
+
|
|
866
|
+
generator = ModularReportGenerator(
|
|
867
|
+
args.workspace,
|
|
868
|
+
args.temp_dir,
|
|
869
|
+
args.apk_name,
|
|
870
|
+
args.sha256
|
|
871
|
+
)
|
|
872
|
+
|
|
873
|
+
# Generate modular markdown reports
|
|
874
|
+
output_md = Path(args.workspace) / 'reports' / args.output_md
|
|
875
|
+
generator.generate_main_report(output_md)
|
|
876
|
+
print(f"✓ Main report: {output_md}")
|
|
877
|
+
print(f"✓ Detailed reports in: {Path(args.workspace) / 'reports'}")
|
|
878
|
+
|
|
879
|
+
# Generate JSON report
|
|
880
|
+
output_json = Path(args.workspace) / 'reports' / args.output_json
|
|
881
|
+
generator.generate_json(output_json)
|
|
882
|
+
print(f"✓ JSON report: {output_json}")
|
|
883
|
+
|
|
884
|
+
if __name__ == '__main__':
|
|
885
|
+
main()
|