|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# -*- coding: utf-8 -*- |
| 3 | + |
| 4 | +import os, datetime, sys, re |
| 5 | +from bs4 import BeautifulSoup |
| 6 | +from pathlib import Path |
| 7 | +from dotenv import load_dotenv |
| 8 | +from shared_utils import appender, errlog |
| 9 | +from urllib.parse import urlparse |
| 10 | +import pycountry |
| 11 | + |
| 12 | +env_path = Path("../.env") |
| 13 | +load_dotenv(dotenv_path=env_path) |
| 14 | +home = os.getenv("RANSOMWARELIVE_HOME") |
| 15 | +tmp_dir = Path(home + os.getenv("TMP_DIR")) |
| 16 | + |
| 17 | +# ---------- helpers ---------- |
| 18 | +SPACE_RE = re.compile(r"\s+") |
| 19 | +PAREN_RE = re.compile(r"\(([^)]+)\)") |
| 20 | +SPLIT_COUNTRY_RE = re.compile(r"[,;/]|\band\b", flags=re.I) |
| 21 | + |
| 22 | +def normspace(s: str) -> str: |
| 23 | + return SPACE_RE.sub(" ", (s or "").strip()) |
| 24 | + |
| 25 | +def clean_title(h4) -> str: |
| 26 | + if not h4: |
| 27 | + return "" |
| 28 | + for img in h4.find_all("img"): |
| 29 | + img.decompose() |
| 30 | + return normspace(h4.get_text(" ", strip=True)) |
| 31 | + |
| 32 | +def alpha3_to_alpha2(code: str) -> str: |
| 33 | + """Convert ISO-3166 alpha-3 to alpha-2 if possible.""" |
| 34 | + try: |
| 35 | + return pycountry.countries.get(alpha_3=code).alpha_2 |
| 36 | + except Exception: |
| 37 | + return code # fallback if not found |
| 38 | + |
| 39 | +def extract_1st_country(title: str) -> str: |
| 40 | + if not title: |
| 41 | + return "" |
| 42 | + m = PAREN_RE.search(title) |
| 43 | + if not m: |
| 44 | + return "" |
| 45 | + inside = normspace(m.group(1)) |
| 46 | + first_token = SPLIT_COUNTRY_RE.split(inside)[0].strip() |
| 47 | + m_iso = re.search(r"\b([A-Z]{2,3})\b", first_token) |
| 48 | + if m_iso: |
| 49 | + code = m_iso.group(1) |
| 50 | + if len(code) == 3: # Convert 3-letter to 2-letter |
| 51 | + return alpha3_to_alpha2(code) |
| 52 | + return code |
| 53 | + return first_token |
| 54 | + |
| 55 | +def strip_country_from_title(title: str) -> str: |
| 56 | + if not title: |
| 57 | + return "" |
| 58 | + return normspace(re.sub(r"\s*\([^)]*\)\s*$", "", title)) |
| 59 | + |
| 60 | +def get_website(card) -> str: |
| 61 | + for h6 in card.find_all("h6"): |
| 62 | + txt = h6.get_text(" ", strip=True).lower() |
| 63 | + if "web site" in txt: |
| 64 | + a = h6.find("a", href=True) |
| 65 | + if a and a.get("href"): |
| 66 | + href = a["href"].strip() |
| 67 | + # Normalize to include scheme if missing |
| 68 | + if not re.match(r"^https?://", href, flags=re.I): |
| 69 | + href = "https://" + href.lstrip() |
| 70 | + try: |
| 71 | + return re.sub(r"^www\.", "", urlparse(href).netloc.lower()) |
| 72 | + except Exception: |
| 73 | + return "" |
| 74 | + return "" |
| 75 | + |
| 76 | + |
| 77 | +def get_description_before_hr(card) -> str: |
| 78 | + desc_parts = [] |
| 79 | + for child in card.children: |
| 80 | + name = getattr(child, "name", None) |
| 81 | + if name == "hr": |
| 82 | + break |
| 83 | + if name == "p": |
| 84 | + cls = child.get("class") or [] |
| 85 | + if "card-text" in cls: |
| 86 | + t = normspace(child.get_text(" ", strip=True)) |
| 87 | + if t: |
| 88 | + desc_parts.append(t) |
| 89 | + return normspace(" ".join(desc_parts)) |
| 90 | + |
| 91 | +# ---------- main ---------- |
| 92 | +def main(): |
| 93 | + script_path = os.path.abspath(__file__) |
| 94 | + if os.path.islink(script_path): |
| 95 | + original_path = os.readlink(script_path) |
| 96 | + if not os.path.isabs(original_path): |
| 97 | + original_path = os.path.join(os.path.dirname(script_path), original_path) |
| 98 | + group_name = os.path.basename(original_path).replace('.py','') |
| 99 | + else: |
| 100 | + group_name = os.path.basename(script_path).replace('.py','') |
| 101 | + |
| 102 | + for filename in os.listdir(tmp_dir): |
| 103 | + try: |
| 104 | + if not filename.startswith(group_name + '-'): |
| 105 | + continue |
| 106 | + |
| 107 | + html_doc = tmp_dir / filename |
| 108 | + with open(html_doc, 'r', encoding='utf-8') as file: |
| 109 | + soup = BeautifulSoup(file, 'html.parser') |
| 110 | + |
| 111 | + cards = soup.find_all("div", class_="card-body") |
| 112 | + for card in cards: |
| 113 | + try: |
| 114 | + h4 = card.find("h4") |
| 115 | + raw_title = clean_title(h4) |
| 116 | + if not raw_title: |
| 117 | + continue |
| 118 | + |
| 119 | + country = extract_1st_country(raw_title) |
| 120 | + victim = strip_country_from_title(raw_title) |
| 121 | + |
| 122 | + website = get_website(card) |
| 123 | + description = get_description_before_hr(card) |
| 124 | + |
| 125 | + |
| 126 | + if not description: |
| 127 | + continue |
| 128 | + |
| 129 | + appender( |
| 130 | + victim=victim, |
| 131 | + group_name=group_name, |
| 132 | + description=description, |
| 133 | + website=website, |
| 134 | + published="", |
| 135 | + post_url="", |
| 136 | + country=country |
| 137 | + ) |
| 138 | + |
| 139 | + except Exception as e_card: |
| 140 | + errlog(group_name + ' - card parse error: ' + str(e_card) + ' in file: ' + filename) |
| 141 | + |
| 142 | + except Exception as e: |
| 143 | + errlog(group_name + ' - parsing fail with error: ' + str(e) + ' in file: ' + filename) |
| 144 | + |
| 145 | +if __name__ == "__main__": |
| 146 | + main() |
0 commit comments