Spaces:
Paused
Paused
Update app.py
Browse files
app.py
CHANGED
|
@@ -8,6 +8,7 @@ import shutil
|
|
| 8 |
import zipfile
|
| 9 |
import base64
|
| 10 |
import asyncio
|
|
|
|
| 11 |
import subprocess
|
| 12 |
from datetime import datetime
|
| 13 |
from typing import List, Dict, Any, Tuple
|
|
@@ -25,14 +26,12 @@ os.makedirs(OUTPUT_DIR, exist_ok=True)
|
|
| 25 |
|
| 26 |
templates = Jinja2Templates(directory="templates")
|
| 27 |
|
| 28 |
-
# الامتدادات المظلمة المستبعدة توفيراً للوقت
|
| 29 |
IGNORED_EXTENSIONS = {
|
| 30 |
'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.ico',
|
| 31 |
'.mp4', '.avi', '.mkv', '.mov', '.flv', '.wmv', '.mp3', '.wav', '.ogg',
|
| 32 |
'.ttf', '.otf', '.woff', '.woff2', '.eot', '.pdf'
|
| 33 |
}
|
| 34 |
|
| 35 |
-
# المجلدات الواهية المستبعدة (تصاميم وأيقونات لا تحتوي كوداً)
|
| 36 |
IGNORED_SUBDIRS = [
|
| 37 |
'res/drawable', 'res/color', 'res/anim', 'res/mipmap', 'res/values-'
|
| 38 |
]
|
|
@@ -67,7 +66,7 @@ class ConnectionManager:
|
|
| 67 |
"message": message,
|
| 68 |
"progress": progress
|
| 69 |
}
|
| 70 |
-
for connection in self.active_connections:
|
| 71 |
try:
|
| 72 |
await connection.send_json(log_entry)
|
| 73 |
except Exception:
|
|
@@ -85,26 +84,18 @@ def clean_workspace():
|
|
| 85 |
elif os.path.isdir(file_path):
|
| 86 |
shutil.rmtree(file_path)
|
| 87 |
except Exception as e:
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
def
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
for b in data:
|
| 97 |
-
byte_counts[b] += 1
|
| 98 |
-
for count in byte_counts:
|
| 99 |
-
if count == 0:
|
| 100 |
-
continue
|
| 101 |
-
p = count / length
|
| 102 |
-
entropy -= p * math.log2(p)
|
| 103 |
-
return entropy
|
| 104 |
|
| 105 |
def extract_strings_from_binary_file(file_path: str) -> str:
|
| 106 |
try:
|
| 107 |
-
res = subprocess.run(['strings', '-n', '4', file_path], capture_output=True, text=True, errors='ignore')
|
| 108 |
return res.stdout
|
| 109 |
except Exception:
|
| 110 |
try:
|
|
@@ -115,36 +106,10 @@ def extract_strings_from_binary_file(file_path: str) -> str:
|
|
| 115 |
except Exception:
|
| 116 |
return ""
|
| 117 |
|
| 118 |
-
def attempt_decompression_and_xor(data: bytes) -> List[Tuple[str, str]]:
|
| 119 |
-
extracted_texts = []
|
| 120 |
-
try:
|
| 121 |
-
decompressed = zlib.decompress(data)
|
| 122 |
-
extracted_texts.append(("Zlib Decompressed", decompressed.decode('utf-8', errors='ignore')))
|
| 123 |
-
except Exception:
|
| 124 |
-
pass
|
| 125 |
-
|
| 126 |
-
try:
|
| 127 |
-
decompressed = gzip.decompress(data)
|
| 128 |
-
extracted_texts.append(("Gzip Decompressed", decompressed.decode('utf-8', errors='ignore')))
|
| 129 |
-
except Exception:
|
| 130 |
-
pass
|
| 131 |
-
|
| 132 |
-
for key in XOR_KEYS:
|
| 133 |
-
try:
|
| 134 |
-
xor_bytes = bytes([b ^ key for b in data[:50000]])
|
| 135 |
-
decoded = xor_bytes.decode('utf-8', errors='ignore')
|
| 136 |
-
if any(k in decoded.lower() for k in ['http', 'discord', 'key', 'license', 'stuart']):
|
| 137 |
-
extracted_texts.append((f"XOR Key (0x{key:02X})", decoded))
|
| 138 |
-
except Exception:
|
| 139 |
-
pass
|
| 140 |
-
|
| 141 |
-
return extracted_texts
|
| 142 |
-
|
| 143 |
def extract_snippet(text: str, start: int, end: int, margin: int = 50) -> str:
|
| 144 |
s_start = max(0, start - margin)
|
| 145 |
s_end = min(len(text), end + margin)
|
| 146 |
-
|
| 147 |
-
return f"...{snippet}..."
|
| 148 |
|
| 149 |
def classify_severity(match_type: str, match_value: str) -> str:
|
| 150 |
val_lower = match_value.lower()
|
|
@@ -180,22 +145,23 @@ async def analyze_zip(file: UploadFile = File(...)):
|
|
| 180 |
extract_folder = os.path.join(WORKSPACE_DIR, "extracted")
|
| 181 |
os.makedirs(extract_folder, exist_ok=True)
|
| 182 |
|
| 183 |
-
|
|
|
|
| 184 |
with open(zip_path, "wb") as buffer:
|
| 185 |
-
while chunk := await file.read(1024 * 1024):
|
| 186 |
buffer.write(chunk)
|
| 187 |
|
| 188 |
-
|
|
|
|
| 189 |
try:
|
| 190 |
-
|
| 191 |
-
zip_file_list = zip_ref.namelist()
|
| 192 |
-
zip_ref.extractall(extract_folder)
|
| 193 |
await manager.broadcast_log("SUCCESS", f"تم فك الضغط بنجاح! إجمالي المخرجات: {len(zip_file_list)} ملف", 25)
|
| 194 |
except Exception as e:
|
| 195 |
return {"status": "error", "message": f"فشل فك ضغط الأرشيف: {str(e)}"}
|
| 196 |
|
|
|
|
| 197 |
files_to_process = []
|
| 198 |
-
max_file_size =
|
| 199 |
|
| 200 |
for root, _, files in os.walk(extract_folder):
|
| 201 |
for f in files:
|
|
@@ -206,7 +172,6 @@ async def analyze_zip(file: UploadFile = File(...)):
|
|
| 206 |
full_path = os.path.join(root, f)
|
| 207 |
rel_p = os.path.relpath(full_path, extract_folder).replace('\\', '/')
|
| 208 |
|
| 209 |
-
# استبعاد مجلدات الموارد غير البرمجية لتسريع الفحص
|
| 210 |
if any(rel_p.startswith(sub) for sub in IGNORED_SUBDIRS):
|
| 211 |
continue
|
| 212 |
|
|
@@ -224,16 +189,22 @@ async def analyze_zip(file: UploadFile = File(...)):
|
|
| 224 |
"medium_priority_count": 0,
|
| 225 |
"low_priority_count": 0,
|
| 226 |
"matches_by_type": {
|
| 227 |
-
"DISCORD": 0, "URL": 0, "EMAIL": 0, "KEYWORD": 0, "BASE64_DECODED": 0
|
| 228 |
}
|
| 229 |
}
|
| 230 |
|
|
|
|
|
|
|
|
|
|
| 231 |
for idx, file_path in enumerate(files_to_process):
|
| 232 |
rel_path = os.path.relpath(file_path, extract_folder)
|
| 233 |
|
| 234 |
-
|
|
|
|
|
|
|
| 235 |
progress = 30 + int((idx / total_files) * 65)
|
| 236 |
-
await manager.broadcast_log("INFO", f"
|
|
|
|
| 237 |
await asyncio.sleep(0.001)
|
| 238 |
|
| 239 |
file_findings = []
|
|
@@ -242,7 +213,6 @@ async def analyze_zip(file: UploadFile = File(...)):
|
|
| 242 |
with open(file_path, 'rb') as f:
|
| 243 |
raw_bytes = f.read()
|
| 244 |
|
| 245 |
-
file_entropy = calculate_entropy(raw_bytes)
|
| 246 |
is_binary = b'\x00' in raw_bytes[:1024] or rel_path.endswith(('.so', '.dex', '.dat'))
|
| 247 |
|
| 248 |
if is_binary:
|
|
@@ -288,7 +258,7 @@ async def analyze_zip(file: UploadFile = File(...)):
|
|
| 288 |
stats[f"{sev.lower()}_priority_count"] += 1
|
| 289 |
|
| 290 |
# Base64
|
| 291 |
-
for b64_match in BASE64_PATTERN.findall(content_text)[:
|
| 292 |
try:
|
| 293 |
decoded = base64.b64decode(b64_match).decode('utf-8', errors='ignore')
|
| 294 |
if any(k in decoded.lower() for k in ['http', 'discord', 'key', 'stuart', 'license', 'verify']):
|
|
@@ -303,20 +273,6 @@ async def analyze_zip(file: UploadFile = File(...)):
|
|
| 303 |
except Exception:
|
| 304 |
pass
|
| 305 |
|
| 306 |
-
if file_entropy > 7.0:
|
| 307 |
-
unpacked = attempt_decompression_and_xor(raw_bytes)
|
| 308 |
-
for method, text_data in unpacked:
|
| 309 |
-
for match in DISCORD_PATTERN.finditer(text_data):
|
| 310 |
-
val = match.group()
|
| 311 |
-
file_findings.append({
|
| 312 |
-
"type": "UNPACKED_XOR",
|
| 313 |
-
"value": f"[{method}] {val}",
|
| 314 |
-
"snippet": f"مستخرج عبر {method}: {val}",
|
| 315 |
-
"severity": "HIGH"
|
| 316 |
-
})
|
| 317 |
-
stats["matches_by_type"]["UNPACKED_XOR"] += 1
|
| 318 |
-
stats["high_priority_count"] += 1
|
| 319 |
-
|
| 320 |
if file_findings:
|
| 321 |
stats["total_matches"] += len(file_findings)
|
| 322 |
findings_by_file.append({
|
|
@@ -325,7 +281,7 @@ async def analyze_zip(file: UploadFile = File(...)):
|
|
| 325 |
"results": file_findings
|
| 326 |
})
|
| 327 |
|
| 328 |
-
except Exception
|
| 329 |
pass
|
| 330 |
|
| 331 |
await manager.broadcast_log("SUCCESS", f"اكتمل الفحص بنجاح! إجمالي التطابقات المكتشفة: {stats['total_matches']}", 100)
|
|
|
|
| 8 |
import zipfile
|
| 9 |
import base64
|
| 10 |
import asyncio
|
| 11 |
+
import time
|
| 12 |
import subprocess
|
| 13 |
from datetime import datetime
|
| 14 |
from typing import List, Dict, Any, Tuple
|
|
|
|
| 26 |
|
| 27 |
templates = Jinja2Templates(directory="templates")
|
| 28 |
|
|
|
|
| 29 |
IGNORED_EXTENSIONS = {
|
| 30 |
'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.ico',
|
| 31 |
'.mp4', '.avi', '.mkv', '.mov', '.flv', '.wmv', '.mp3', '.wav', '.ogg',
|
| 32 |
'.ttf', '.otf', '.woff', '.woff2', '.eot', '.pdf'
|
| 33 |
}
|
| 34 |
|
|
|
|
| 35 |
IGNORED_SUBDIRS = [
|
| 36 |
'res/drawable', 'res/color', 'res/anim', 'res/mipmap', 'res/values-'
|
| 37 |
]
|
|
|
|
| 66 |
"message": message,
|
| 67 |
"progress": progress
|
| 68 |
}
|
| 69 |
+
for connection in list(self.active_connections):
|
| 70 |
try:
|
| 71 |
await connection.send_json(log_entry)
|
| 72 |
except Exception:
|
|
|
|
| 84 |
elif os.path.isdir(file_path):
|
| 85 |
shutil.rmtree(file_path)
|
| 86 |
except Exception as e:
|
| 87 |
+
pass
|
| 88 |
+
|
| 89 |
+
def extract_zip_sync(zip_path: str, extract_to: str) -> List[str]:
|
| 90 |
+
"""فك ضغط الملف بمسار مستقل عن Event Loop لمنع التجمّد"""
|
| 91 |
+
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
| 92 |
+
file_list = zip_ref.namelist()
|
| 93 |
+
zip_ref.extractall(extract_to)
|
| 94 |
+
return file_list
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
|
| 96 |
def extract_strings_from_binary_file(file_path: str) -> str:
|
| 97 |
try:
|
| 98 |
+
res = subprocess.run(['strings', '-n', '4', file_path], capture_output=True, text=True, errors='ignore', timeout=5)
|
| 99 |
return res.stdout
|
| 100 |
except Exception:
|
| 101 |
try:
|
|
|
|
| 106 |
except Exception:
|
| 107 |
return ""
|
| 108 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
def extract_snippet(text: str, start: int, end: int, margin: int = 50) -> str:
|
| 110 |
s_start = max(0, start - margin)
|
| 111 |
s_end = min(len(text), end + margin)
|
| 112 |
+
return f"...{text[s_start:s_end].replace('\n', ' ').replace('\r', ' ')}..."
|
|
|
|
| 113 |
|
| 114 |
def classify_severity(match_type: str, match_value: str) -> str:
|
| 115 |
val_lower = match_value.lower()
|
|
|
|
| 145 |
extract_folder = os.path.join(WORKSPACE_DIR, "extracted")
|
| 146 |
os.makedirs(extract_folder, exist_ok=True)
|
| 147 |
|
| 148 |
+
# 1. حفظ الملف بسلاسة لتجنب احتكار الذاكرة
|
| 149 |
+
await manager.broadcast_log("INFO", f"جاري استلام الملف: {file.filename}...", 5)
|
| 150 |
with open(zip_path, "wb") as buffer:
|
| 151 |
+
while chunk := await file.read(2 * 1024 * 1024): # قراءة بإنتاجية 2MB
|
| 152 |
buffer.write(chunk)
|
| 153 |
|
| 154 |
+
# 2. فك الضغط بـ Thread خفي لمنع تجمّد السيرفر عند 12%
|
| 155 |
+
await manager.broadcast_log("INFO", "جاري فك ضغط الأرشيف عبر الأنوية متعددة...", 15)
|
| 156 |
try:
|
| 157 |
+
zip_file_list = await asyncio.to_thread(extract_zip_sync, zip_path, extract_folder)
|
|
|
|
|
|
|
| 158 |
await manager.broadcast_log("SUCCESS", f"تم فك الضغط بنجاح! إجمالي المخرجات: {len(zip_file_list)} ملف", 25)
|
| 159 |
except Exception as e:
|
| 160 |
return {"status": "error", "message": f"فشل فك ضغط الأرشيف: {str(e)}"}
|
| 161 |
|
| 162 |
+
# 3. تجميع الملفات المفلترة
|
| 163 |
files_to_process = []
|
| 164 |
+
max_file_size = 30 * 1024 * 1024 # حد أقصى 30MB للملف الواحد لتفادي البطء
|
| 165 |
|
| 166 |
for root, _, files in os.walk(extract_folder):
|
| 167 |
for f in files:
|
|
|
|
| 172 |
full_path = os.path.join(root, f)
|
| 173 |
rel_p = os.path.relpath(full_path, extract_folder).replace('\\', '/')
|
| 174 |
|
|
|
|
| 175 |
if any(rel_p.startswith(sub) for sub in IGNORED_SUBDIRS):
|
| 176 |
continue
|
| 177 |
|
|
|
|
| 189 |
"medium_priority_count": 0,
|
| 190 |
"low_priority_count": 0,
|
| 191 |
"matches_by_type": {
|
| 192 |
+
"DISCORD": 0, "URL": 0, "EMAIL": 0, "KEYWORD": 0, "BASE64_DECODED": 0
|
| 193 |
}
|
| 194 |
}
|
| 195 |
|
| 196 |
+
last_log_time = time.time()
|
| 197 |
+
|
| 198 |
+
# 4. مسح وتسريع الفحص
|
| 199 |
for idx, file_path in enumerate(files_to_process):
|
| 200 |
rel_path = os.path.relpath(file_path, extract_folder)
|
| 201 |
|
| 202 |
+
# إرسال تحديث السجل كل 200 ملي ثانية فقط لحماية المتصفح من التجمد
|
| 203 |
+
current_time = time.time()
|
| 204 |
+
if (current_time - last_log_time > 0.2) or idx == total_files - 1:
|
| 205 |
progress = 30 + int((idx / total_files) * 65)
|
| 206 |
+
await manager.broadcast_log("INFO", f"تفتيش ({idx + 1}/{total_files}): {rel_path}", progress)
|
| 207 |
+
last_log_time = current_time
|
| 208 |
await asyncio.sleep(0.001)
|
| 209 |
|
| 210 |
file_findings = []
|
|
|
|
| 213 |
with open(file_path, 'rb') as f:
|
| 214 |
raw_bytes = f.read()
|
| 215 |
|
|
|
|
| 216 |
is_binary = b'\x00' in raw_bytes[:1024] or rel_path.endswith(('.so', '.dex', '.dat'))
|
| 217 |
|
| 218 |
if is_binary:
|
|
|
|
| 258 |
stats[f"{sev.lower()}_priority_count"] += 1
|
| 259 |
|
| 260 |
# Base64
|
| 261 |
+
for b64_match in BASE64_PATTERN.findall(content_text)[:5]:
|
| 262 |
try:
|
| 263 |
decoded = base64.b64decode(b64_match).decode('utf-8', errors='ignore')
|
| 264 |
if any(k in decoded.lower() for k in ['http', 'discord', 'key', 'stuart', 'license', 'verify']):
|
|
|
|
| 273 |
except Exception:
|
| 274 |
pass
|
| 275 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 276 |
if file_findings:
|
| 277 |
stats["total_matches"] += len(file_findings)
|
| 278 |
findings_by_file.append({
|
|
|
|
| 281 |
"results": file_findings
|
| 282 |
})
|
| 283 |
|
| 284 |
+
except Exception:
|
| 285 |
pass
|
| 286 |
|
| 287 |
await manager.broadcast_log("SUCCESS", f"اكتمل الفحص بنجاح! إجمالي التطابقات المكتشفة: {stats['total_matches']}", 100)
|