Rohithm16 commited on
Commit
94df088
·
verified ·
1 Parent(s): cf52735

Upload 4 files

Browse files
Files changed (4) hide show
  1. Dockerfile +34 -0
  2. app.py +67 -0
  3. ml_pipeline.py +447 -0
  4. requirements.txt +0 -0
Dockerfile ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use an official lightweight Python image
2
+ FROM python:3.10-slim
3
+
4
+ # Create a non-root user that Hugging Face requires (User ID 1000)
5
+ RUN useradd -m -u 1000 user
6
+ USER user
7
+
8
+ # Set up the environment paths for the new user
9
+ ENV HOME=/home/user \
10
+ PATH=/home/user/.local/bin:$PATH
11
+
12
+ # Set the working directory
13
+ WORKDIR $HOME/app
14
+
15
+ # Copy your requirements and install them
16
+ COPY --chown=user requirements.txt .
17
+ RUN pip install --no-cache-dir -r requirements.txt
18
+
19
+ # Copy all your application code and models into the container
20
+ COPY --chown=user . .
21
+
22
+ # Switch to root temporarily to create the /tmp/ folders and grant open permissions
23
+ USER root
24
+ RUN mkdir -p /tmp/tag_and_trail_downloads /tmp/tag_and_trail_uploads && \
25
+ chmod -R 777 /tmp/tag_and_trail_downloads /tmp/tag_and_trail_uploads
26
+
27
+ # Switch back to the safe user
28
+ USER user
29
+
30
+ # Expose Hugging Face's mandatory port
31
+ EXPOSE 7860
32
+
33
+ # Command to boot up your ML Brain
34
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
app.py ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import shutil
3
+ from fastapi import FastAPI, UploadFile, File, HTTPException
4
+ from pydantic import BaseModel
5
+
6
+ # Import the master pipelines we just built
7
+ from ml_pipeline import process_url_pipeline, process_media_pipeline
8
+
9
+ app = FastAPI(title="Tag & Trail ML Brain")
10
+
11
+ # Pydantic model for incoming URL requests
12
+ class URLRequest(BaseModel):
13
+ text: str
14
+
15
+ @app.post("/predict_url")
16
+ async def predict_url(req: URLRequest):
17
+ """Receives a URL, runs ML, and extracts metadata if safe."""
18
+ if not req.text:
19
+ raise HTTPException(status_code=400, detail="No text/URL provided.")
20
+
21
+ try:
22
+ result = process_url_pipeline(req.text)
23
+
24
+ # Ensure we return a "prediction" key so your helpers.py can read it perfectly
25
+ if "class" in result:
26
+ result["prediction"] = result["class"].lower()
27
+
28
+ return result
29
+ except Exception as e:
30
+ raise HTTPException(status_code=500, detail=f"URL Pipeline Error: {str(e)}")
31
+
32
+ @app.post("/predict_pdf")
33
+ async def predict_pdf(file: UploadFile = File(...)):
34
+ """Receives a PDF/Media file, converts, runs ML, and extracts metadata if safe."""
35
+
36
+ # Hugging Face Spaces allows writing to /tmp/ for ephemeral storage
37
+ temp_dir = "/tmp/tag_and_trail_downloads"
38
+ os.makedirs(temp_dir, exist_ok=True)
39
+
40
+ temp_path = os.path.join(temp_dir, file.filename)
41
+
42
+ try:
43
+ # Save the incoming file from Twilio/helpers.py
44
+ with open(temp_path, "wb") as buffer:
45
+ shutil.copyfileobj(file.file, buffer)
46
+
47
+ # Run your master media pipeline
48
+ result = process_media_pipeline(
49
+ file_path=temp_path,
50
+ mime=file.content_type,
51
+ original_url_or_name=file.filename
52
+ )
53
+
54
+ return result
55
+
56
+ except Exception as e:
57
+ raise HTTPException(status_code=500, detail=f"Media Pipeline Error: {str(e)}")
58
+
59
+ finally:
60
+ # ALWAYS clean up to prevent memory/storage leaks in Hugging Face
61
+ if os.path.exists(temp_path):
62
+ os.remove(temp_path)
63
+
64
+ # Root endpoint just for quick health checks
65
+ @app.get("/")
66
+ def health_check():
67
+ return {"status": "Tag & Trail ML API is running smoothly!"}
ml_pipeline.py ADDED
@@ -0,0 +1,447 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import io
3
+ import re
4
+ import json
5
+ import math
6
+ import uuid
7
+ import fitz
8
+ import docx
9
+ import joblib
10
+ import requests
11
+ import numpy as np
12
+ import pandas as pd
13
+
14
+ from pptx import Presentation
15
+ from openpyxl import load_workbook
16
+ from PIL import Image
17
+ from bs4 import BeautifulSoup
18
+ from PyPDF2 import PdfReader
19
+
20
+ # TensorFlow Imports
21
+ from tensorflow.keras.models import load_model
22
+ from tensorflow.keras.preprocessing.text import tokenizer_from_json
23
+ from tensorflow.keras.preprocessing.sequence import pad_sequences
24
+
25
+ os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"
26
+ os.environ["TF_ENABLE_ONEDNN_OPTS"] = "0"
27
+
28
+ BASE_DIR = os.path.dirname(__file__)
29
+
30
+ # Hugging Face usually provides a writeable /tmp/ directory for ephemeral files
31
+ DOWNLOAD_FOLDER = "/tmp/tag_and_trail_downloads"
32
+ os.makedirs(DOWNLOAD_FOLDER, exist_ok=True)
33
+
34
+ # ==============================================================================
35
+ # 1. URL ML MODEL (LSTM + CNN)
36
+ # ==============================================================================
37
+
38
+ tokenizer_path = os.path.join(BASE_DIR, "models", "tokenizer.json")
39
+ with open(tokenizer_path, 'r') as f:
40
+ tokenizer_data = json.load(f)
41
+ tokenizer = tokenizer_from_json(tokenizer_data)
42
+
43
+ lstm_model = load_model(os.path.join(BASE_DIR, 'models', 'tag_and_trail_lstm_best.keras'))
44
+ cnn_model = load_model(os.path.join(BASE_DIR, 'models', 'tag_trail_url_cnn_model.keras'))
45
+
46
+ def tag_and_trail_inference(raw_url):
47
+ url = raw_url.lower().strip()
48
+ sequences = tokenizer.texts_to_sequences([url])
49
+ padded_data = pad_sequences(sequences, maxlen=200, padding='post')
50
+
51
+ lstm_preds = lstm_model.predict(padded_data, verbose=0)
52
+ cnn_preds = cnn_model.predict(padded_data, verbose=0)
53
+
54
+ final_probabilities = (lstm_preds + cnn_preds) / 2
55
+
56
+ classes = ['Safe', 'Defacement', 'Malware', 'Phishing']
57
+ predicted_idx = np.argmax(final_probabilities)
58
+ confidence = float(np.max(final_probabilities))
59
+
60
+ return {
61
+ "url": raw_url,
62
+ "class": classes[predicted_idx],
63
+ "confidence": confidence,
64
+ "raw_scores": final_probabilities.tolist()
65
+ }
66
+
67
+
68
+ # ==============================================================================
69
+ # 2. PDF ML MODEL (Feature Extraction + Joblib)
70
+ # ==============================================================================
71
+
72
+ FEATURE_NAMES = ["pdfsize", "pages", "isEncrypted", "JS", "OpenAction", "launch", "AA", "EmbeddedFile", "ObjStm", "entropy"]
73
+ SUSPICIOUS_KEYS = [b"/JS", b"/JavaScript", b"/OpenAction", b"/Launch", b"/AA", b"/EmbeddedFile", b"/ObjStm"]
74
+ FLAG_MAPPING = {3: "JavaScript", 4: "OpenAction", 5: "Launch", 6: "AdditionalActions", 7: "EmbeddedFile", 8: "ObjectStream"}
75
+
76
+ MODEL_PATH = os.path.join(BASE_DIR, "models", "model.joblib")
77
+ try:
78
+ bundle = joblib.load(MODEL_PATH)
79
+ pdf_model = bundle["model"]
80
+ FEATURE_COLUMNS = bundle["features"]
81
+ scaler = bundle.get("scaler", None)
82
+ print("PDF model loaded successfully")
83
+ except Exception as e:
84
+ print("PDF model load error:", e)
85
+ pdf_model = None
86
+ scaler = None
87
+ FEATURE_COLUMNS = FEATURE_NAMES
88
+
89
+ def shannon_entropy(data: bytes):
90
+ if not data: return 0.0
91
+ freq = {}
92
+ for b in data: freq[b] = freq.get(b, 0) + 1
93
+ entropy = 0.0
94
+ length = len(data)
95
+ for count in freq.values():
96
+ p = count / length
97
+ entropy -= p * math.log2(p)
98
+ return entropy
99
+
100
+ def extract_features(pdf_path):
101
+ try:
102
+ pdfsize = os.path.getsize(pdf_path) / 1024.0
103
+ with open(pdf_path, "rb") as f:
104
+ raw = f.read(1024 * 1024)
105
+ entropy = shannon_entropy(raw)
106
+ reader = PdfReader(pdf_path, strict=False)
107
+ pages = len(reader.pages)
108
+ is_encrypted = 1 if reader.is_encrypted else 0
109
+ counts = {k.decode('latin-1'): raw.count(k) for k in SUSPICIOUS_KEYS}
110
+ features = [
111
+ pdfsize, float(pages), float(is_encrypted),
112
+ float(counts.get('/JS', 0) + counts.get('/JavaScript', 0)),
113
+ float(1 if counts.get('/OpenAction', 0) > 0 else 0),
114
+ float(1 if counts.get('/Launch', 0) > 0 else 0),
115
+ float(1 if counts.get('/AA', 0) > 0 else 0),
116
+ float(1 if counts.get('/EmbeddedFile', 0) > 0 else 0),
117
+ float(1 if counts.get('/ObjStm', 0) > 0 else 0),
118
+ entropy
119
+ ]
120
+ features[6] = min(features[6], 0.3) # AA
121
+ features[8] = min(features[8], 0.3) # ObjStm
122
+ return features
123
+ except Exception:
124
+ return [0.0] * len(FEATURE_NAMES)
125
+
126
+ def check_pdf_encryption(pdf_path):
127
+ try:
128
+ reader = PdfReader(pdf_path, strict=False)
129
+ return reader.is_encrypted
130
+ except:
131
+ return False
132
+
133
+ def extract_flags(features):
134
+ flags = []
135
+ for index, name in FLAG_MAPPING.items():
136
+ if features[index] > 0: flags.append(name)
137
+ return flags
138
+
139
+ def predict_pdf(pdf_path):
140
+ if not os.path.exists(pdf_path):
141
+ return {"prediction": "UNKNOWN", "malicious_probability": 0.0, "risk_level": "Error", "flags": []}
142
+
143
+ is_encrypted = check_pdf_encryption(pdf_path)
144
+ features = extract_features(pdf_path)
145
+ flags = extract_flags(features)
146
+
147
+ if pdf_model is None:
148
+ return {"prediction": "UNKNOWN", "malicious_probability": 0.0, "risk_level": "Error", "flags": flags}
149
+ try:
150
+ X = pd.DataFrame([features], columns=FEATURE_COLUMNS)
151
+ if scaler: X_scaled = scaler.transform(X)
152
+ else: X_scaled = X.values
153
+
154
+ prob_malicious = float(pdf_model.predict_proba(X_scaled)[0][1])
155
+
156
+ js = features[3]
157
+ openaction = features[4]
158
+ launch = features[5]
159
+ aa = features[6]
160
+ embedded = features[7]
161
+
162
+ if (js == 0 and openaction == 0 and launch == 0 and embedded == 0 and not is_encrypted):
163
+ prob_malicious = 0.1
164
+ strong_flags = js + openaction + launch + embedded
165
+ if strong_flags <=1 and prob_malicious > 0.5:
166
+ prob_malicious = 0.3
167
+
168
+ if prob_malicious >= 0.85: risk_level = "HIGH"
169
+ elif prob_malicious >= 0.6: risk_level = "MEDIUM"
170
+ elif prob_malicious >= 0.3: risk_level = "LOW"
171
+ else: risk_level = "SAFE"
172
+
173
+ return {
174
+ "prediction": "Malicious" if prob_malicious > 0.5 else "Safe",
175
+ "malicious_probability": round(prob_malicious, 4),
176
+ "risk_level": risk_level,
177
+ "flags": flags,
178
+ "is_encrypted": is_encrypted
179
+ }
180
+ except Exception as e:
181
+ return {"prediction": "UNKNOWN", "malicious_probability": 0.0, "risk_level": "Error", "flags": flags, "error": str(e)}
182
+
183
+
184
+ # ==============================================================================
185
+ # 3. CONVERTERS
186
+ # ==============================================================================
187
+
188
+ def convert_image_to_pdf(image_path: str) -> str:
189
+ img = Image.open(image_path)
190
+ if img.mode != "RGB": img = img.convert("RGB")
191
+ img_bytes = io.BytesIO()
192
+ img.save(img_bytes, format="PDF")
193
+ pdf_path = os.path.join(DOWNLOAD_FOLDER, f"{uuid.uuid4()}.pdf")
194
+ with open(pdf_path, "wb") as f: f.write(img_bytes.getvalue())
195
+ return pdf_path
196
+
197
+ def convert_text_to_pdf(text_path: str) -> str:
198
+ with open(text_path, "r", encoding="utf-8", errors="ignore") as f: text = f.read()
199
+ doc = fitz.open()
200
+ page = doc.new_page()
201
+ page.insert_text((50, 50), text, fontsize=11)
202
+ pdf_path = os.path.join(DOWNLOAD_FOLDER, f"{uuid.uuid4()}.pdf")
203
+ doc.save(pdf_path)
204
+ doc.close()
205
+ return pdf_path
206
+
207
+ def convert_docx_to_pdf(docx_path: str) -> str:
208
+ doc = docx.Document(docx_path)
209
+ full_text = [para.text for para in doc.paragraphs]
210
+ text = "\n".join(full_text)
211
+ tmp_path = docx_path + ".tmp.txt"
212
+ with open(tmp_path, "w", encoding="utf-8") as f: f.write(text)
213
+ pdf_path = convert_text_to_pdf(tmp_path)
214
+ os.remove(tmp_path)
215
+ return pdf_path
216
+
217
+ def convert_pptx_to_pdf(pptx_path: str) -> str:
218
+ prs = Presentation(pptx_path)
219
+ text_runs = []
220
+ for slide in prs.slides:
221
+ for shape in slide.shapes:
222
+ if hasattr(shape, "text"): text_runs.append(shape.text)
223
+ text = "\n".join(text_runs)
224
+ tmp_path = pptx_path + ".tmp.txt"
225
+ with open(tmp_path, "w", encoding="utf-8") as f: f.write(text)
226
+ pdf_path = convert_text_to_pdf(tmp_path)
227
+ os.remove(tmp_path)
228
+ return pdf_path
229
+
230
+ def convert_xlsx_to_pdf(xlsx_path: str) -> str:
231
+ wb = load_workbook(xlsx_path, read_only=True, data_only=True)
232
+ text_lines = []
233
+ for sheet in wb.worksheets:
234
+ for row in sheet.iter_rows(values_only=True):
235
+ row_str = " | ".join(str(cell) if cell is not None else "" for cell in row)
236
+ if row_str.strip(): text_lines.append(row_str)
237
+ text = "\n".join(text_lines)
238
+ tmp_path = xlsx_path + ".tmp.txt"
239
+ with open(tmp_path, "w", encoding="utf-8") as f: f.write(text)
240
+ pdf_path = convert_text_to_pdf(tmp_path)
241
+ os.remove(tmp_path)
242
+ return pdf_path
243
+
244
+ def convert_to_pdf(file_path: str, mime: str) -> str:
245
+ if mime.startswith("image/"): return convert_image_to_pdf(file_path)
246
+ elif mime == "text/plain": return convert_text_to_pdf(file_path)
247
+ elif mime == "application/vnd.openxmlformats-officedocument.wordprocessingml.document": return convert_docx_to_pdf(file_path)
248
+ elif mime == "application/msword": raise ValueError("Unsupported mime type: .doc (old format) – please convert to .docx")
249
+ elif mime == "application/vnd.openxmlformats-officedocument.presentationml.presentation": return convert_pptx_to_pdf(file_path)
250
+ elif mime == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": return convert_xlsx_to_pdf(file_path)
251
+ else: return file_path # If already PDF or unknown, pass through
252
+
253
+
254
+ # ==============================================================================
255
+ # 4. METADATA EXTRACTION
256
+ # ==============================================================================
257
+
258
+ def clean_text(text: str, keep_punctuation: bool = True) -> str:
259
+ if not text: return ""
260
+ text = text.lower()
261
+ text = re.sub(r"\[\d+\]", "", text)
262
+ if keep_punctuation: text = re.sub(r"[^a-z0-9\s.,!?;:'\"()-]", " ", text)
263
+ else: text = re.sub(r"[^a-z0-9\s]", " ", text)
264
+ text = re.sub(r"\s+", " ", text)
265
+ return text.strip()
266
+
267
+ def title_from_url(url: str) -> str:
268
+ from urllib.parse import urlparse
269
+ parsed = urlparse(url)
270
+ filename = os.path.splitext(os.path.basename(parsed.path))[0]
271
+ if not filename: return ""
272
+ parts = re.split(r"[-_/]", filename)
273
+ words = [p for p in parts if p and not p.isdigit()]
274
+ return clean_text(" ".join(words), keep_punctuation=False)
275
+
276
+ def is_bad_title(title: str) -> bool:
277
+ if not title: return True
278
+ title_lower = title.lower()
279
+ bad_phrases = ["error", "not found", "404", "403", "access denied", "forbidden", "captcha", "challenge", "just a moment", "enable javascript", "redirecting", "loading"]
280
+ return any(phrase in title_lower for phrase in bad_phrases)
281
+
282
+ def extract_first_paragraph_html(soup):
283
+ for tag in soup(["script", "style", "nav", "footer", "header", "aside"]): tag.decompose()
284
+ content_parents = soup.find_all(['article', 'main'])
285
+ if not content_parents:
286
+ content_parents = soup.find_all('div', class_=re.compile(r"(content|post|entry|article)", re.I))
287
+ if not content_parents: content_parents = [soup]
288
+ best_paragraph = None
289
+ max_len = 0
290
+ for parent in content_parents:
291
+ for p in parent.find_all('p', recursive=True):
292
+ text = clean_text(p.get_text(" ", strip=True))
293
+ length = len(text)
294
+ if length <= 80: continue
295
+ if text.startswith("by ") and length < 120: continue
296
+ if length > 300: return text
297
+ if length > max_len:
298
+ max_len = length
299
+ best_paragraph = text
300
+ if best_paragraph: return best_paragraph
301
+ for p in soup.find_all('p'):
302
+ text = clean_text(p.get_text(" ", strip=True))
303
+ if len(text) > 80: return text
304
+ return None
305
+
306
+ def extract_first_paragraph_pdf(pdf_bytes, max_pages=5):
307
+ doc = fitz.open(stream=pdf_bytes, filetype="pdf")
308
+ try:
309
+ if doc.page_count == 0: return None
310
+ pages_to_scan = min(doc.page_count, max_pages)
311
+ for page_num in range(pages_to_scan):
312
+ page = doc.load_page(page_num)
313
+ text = page.get_text()
314
+ for block in text.split("\n\n"):
315
+ block = block.strip()
316
+ if not block: continue
317
+ cleaned = clean_text(block)
318
+ length = len(cleaned)
319
+ if length <= 80: continue
320
+ digit_ratio = sum(c.isdigit() for c in cleaned) / length
321
+ if digit_ratio > 0.3: continue
322
+ return cleaned
323
+ lines = [line.strip() for line in text.split("\n") if line.strip()]
324
+ combined = ""
325
+ for line in lines:
326
+ combined += " " + line
327
+ if len(combined) > 200:
328
+ cleaned = clean_text(combined)
329
+ length = len(cleaned)
330
+ if length <= 80: continue
331
+ digit_ratio = sum(c.isdigit() for c in cleaned) / length
332
+ if digit_ratio > 0.3: continue
333
+ return cleaned
334
+ return None
335
+ finally:
336
+ doc.close()
337
+
338
+ def extract_metadata_from_url(url: str) -> dict:
339
+ metadata = {"title": None, "description": None}
340
+ try:
341
+ response = requests.get(url, timeout=(5, 10), headers={"User-Agent": "Mozilla/5.0"}, allow_redirects=True)
342
+ response.raise_for_status()
343
+ response.encoding = response.apparent_encoding
344
+ content_type = response.headers.get("Content-Type", "").lower()
345
+ if not content_type.startswith("text/html"):
346
+ metadata["title"] = title_from_url(url)
347
+ return metadata
348
+ soup = BeautifulSoup(response.text, "html.parser")
349
+ title_candidates = []
350
+ og_title = soup.find("meta", property="og:title")
351
+ if og_title and og_title.get("content"): title_candidates.append(clean_text(og_title["content"]))
352
+ if soup.title and soup.title.string: title_candidates.append(clean_text(soup.title.string))
353
+ h1 = soup.find("h1")
354
+ if h1: title_candidates.append(clean_text(h1.get_text()))
355
+ title_candidates.append(title_from_url(url))
356
+ for candidate in title_candidates:
357
+ if candidate and not is_bad_title(candidate):
358
+ if " | " in candidate: candidate = candidate.split(" | ")[0].strip()
359
+ if " - " in candidate: candidate = candidate.split(" - ")[0].strip()
360
+ metadata["title"] = candidate
361
+ break
362
+ desc = None
363
+ meta_desc = soup.find("meta", attrs={"name": re.compile(r"^description$", re.I)})
364
+ if meta_desc and meta_desc.get("content"): desc = clean_text(meta_desc["content"])
365
+ if not desc:
366
+ og_desc = soup.find("meta", property="og:description")
367
+ if og_desc and og_desc.get("content"): desc = clean_text(og_desc["content"])
368
+ if not desc:
369
+ twitter_desc = soup.find("meta", attrs={"name": re.compile(r"^twitter:description$", re.I)})
370
+ if twitter_desc and twitter_desc.get("content"): desc = clean_text(twitter_desc["content"])
371
+ first_para = extract_first_paragraph_html(soup)
372
+ if desc and first_para: metadata["description"] = f"{desc} | {first_para}"
373
+ elif desc: metadata["description"] = desc
374
+ elif first_para: metadata["description"] = first_para
375
+ return metadata
376
+ except requests.RequestException:
377
+ metadata["title"] = title_from_url(url)
378
+ return metadata
379
+
380
+ def extract_metadata_from_pdf(file_path: str, url: str) -> dict:
381
+ metadata = {"title": None, "description": None}
382
+ try:
383
+ doc = fitz.open(file_path)
384
+ try:
385
+ pdf_title = doc.metadata.get("title")
386
+ cleaned_title = clean_text(pdf_title) if pdf_title else None
387
+ if cleaned_title and not is_bad_title(cleaned_title): metadata["title"] = cleaned_title
388
+ else: metadata["title"] = title_from_url(url)
389
+ finally:
390
+ doc.close()
391
+ with open(file_path, "rb") as f: pdf_bytes = f.read()
392
+ first_para = extract_first_paragraph_pdf(pdf_bytes)
393
+ if first_para: metadata["description"] = first_para
394
+ return metadata
395
+ except Exception:
396
+ metadata["title"] = title_from_url(url)
397
+ return metadata
398
+
399
+
400
+ # ==============================================================================
401
+ # 5. THE MASTER PIPELINES (Safety First Rule Enforced)
402
+ # ==============================================================================
403
+
404
+ def process_url_pipeline(url: str) -> dict:
405
+ """Predicts threat first. If safe, extracts metadata for tagging."""
406
+ result = tag_and_trail_inference(url)
407
+
408
+ # If the model finds it malicious, abort metadata extraction to save compute/safety
409
+ if result["class"] in ["Malware", "Phishing"]:
410
+ result["metadata_skipped"] = "Threat detected. Extraction aborted."
411
+ return result
412
+
413
+ # If safe, extract the data
414
+ metadata = extract_metadata_from_url(url)
415
+ result.update(metadata)
416
+
417
+ # (Here is where you will eventually call your HF Tagging API with the metadata)
418
+
419
+ return result
420
+
421
+ def process_media_pipeline(file_path: str, mime: str, original_url_or_name: str) -> dict:
422
+ """Converts to PDF (if needed), predicts threat first. If safe, extracts metadata."""
423
+
424
+ # Step 1: Conversion (The PDF model requires a PDF)
425
+ if mime != "application/pdf":
426
+ try:
427
+ pdf_path = convert_to_pdf(file_path, mime)
428
+ except Exception as e:
429
+ return {"error": f"Conversion failed: {str(e)}"}
430
+ else:
431
+ pdf_path = file_path
432
+
433
+ # Step 2: Machine Learning Prediction
434
+ result = predict_pdf(pdf_path)
435
+
436
+ # Step 3: Safety Check - Only extract if it's Safe
437
+ if result["prediction"] == "Safe" and result["risk_level"] in ["SAFE", "LOW"]:
438
+ metadata = extract_metadata_from_pdf(pdf_path, original_url_or_name)
439
+ result.update(metadata)
440
+ else:
441
+ result["metadata_skipped"] = "Threat detected. Extraction aborted."
442
+
443
+ # Clean up the converted PDF if we created a new one
444
+ if pdf_path != file_path and os.path.exists(pdf_path):
445
+ os.remove(pdf_path)
446
+
447
+ return result
requirements.txt ADDED
Binary file (2.3 kB). View file