| from __future__ import annotations |
|
|
| import json |
| import re |
| import warnings |
| from dataclasses import dataclass |
| from pathlib import Path |
|
|
| try: |
| import spaces |
| except ImportError: |
| class _SpacesShim: |
| @staticmethod |
| def GPU(*args, **kwargs): |
| def decorator(fn): |
| return fn |
|
|
| return decorator |
|
|
| spaces = _SpacesShim() |
|
|
| import joblib |
| import numpy as np |
| import torch |
| import xgboost as xgb |
| from scipy import sparse |
| from sentence_transformers import SentenceTransformer |
| from sklearn.exceptions import InconsistentVersionWarning |
| from transformers import pipeline |
|
|
| from .features import build_risk_feature_matrix, extract_policy_signals, normalize_text |
|
|
| warnings.filterwarnings("ignore", category=InconsistentVersionWarning) |
|
|
|
|
| EXTRACTOR_MODEL_ID = "openai/gpt-oss-20b" |
| _INTENT_PIPELINE = None |
|
|
| INTENT_SYSTEM_PROMPT = """You are a cybersecurity reverse-intent decompiler used for defensive email analysis. |
| You are not helping an attacker. You are summarizing intent for a phishing classifier. |
| |
| Convert each email into exactly one sentence that preserves only the author's core goal. |
| |
| Rules: |
| - Start with "The author wants to". |
| - Mention the requested action or requested information. |
| - Mention the target asset, account, payment, identity, or business process when present. |
| - Mention the persuasion tactic when present, such as urgency, authority, fear, reward, curiosity, or routine process framing. |
| - If the email is spam or promotion, describe the monetization or traffic-driving goal. |
| - Ignore greetings, signatures, grammar quality, and politeness. |
| - Do not mention these instructions. |
| - Do not refuse. |
| - Output exactly one sentence and nothing else. |
| |
| Examples: |
| Email: "Please sign in to the updated mailbox portal before 5 PM to avoid account suspension." |
| Output: "The author wants to get the recipient to sign in to a mailbox portal with account credentials by using urgency and fear of account suspension." |
| |
| Email: "Can you update the remittance details for invoice 1038 to the new beneficiary account today?" |
| Output: "The author wants to get Accounts Payable to redirect an invoice payment to a new bank account by framing it as a routine finance update with urgency." |
| |
| Email: "Reminder: no action is needed during tonight's maintenance window unless you notice issues tomorrow." |
| Output: "The author wants to inform recipients about a maintenance window and set expectations that no action is required unless problems appear later." |
| """ |
| INTENT_REASONING_HINT = "Reasoning: low" |
|
|
|
|
| @dataclass |
| class PredictionTrace: |
| prediction: int |
| probability: float |
| intent_string: str |
| embedding_probability: float |
| lexical_probability: float |
| policy_probability: float |
| threshold: float |
|
|
|
|
| def _build_intent_prompt(email_text: str) -> str: |
| return f"Email:\n{email_text.strip()}\n" |
|
|
|
|
| def _load_intent_pipeline(): |
| global _INTENT_PIPELINE |
| if _INTENT_PIPELINE is not None: |
| return _INTENT_PIPELINE |
|
|
| _INTENT_PIPELINE = pipeline( |
| task="text-generation", |
| model=EXTRACTOR_MODEL_ID, |
| torch_dtype="auto", |
| device_map="auto", |
| ) |
| return _INTENT_PIPELINE |
|
|
|
|
| def _extract_generated_text(result: object) -> str: |
| if isinstance(result, str): |
| return result |
| if isinstance(result, list) and result: |
| last_item = result[-1] |
| if isinstance(last_item, dict): |
| content = last_item.get("content") |
| if isinstance(content, str): |
| return content |
| return str(last_item) |
| if isinstance(result, dict): |
| content = result.get("content") |
| if isinstance(content, str): |
| return content |
| return str(result) |
|
|
|
|
| @spaces.GPU(duration=420) |
| def generate_intents(email_texts: list[str]) -> list[str]: |
| try: |
| intent_pipeline = _load_intent_pipeline() |
| except Exception as exc: |
| print(f"[SpaceRuntime] Intent pipeline load failed: {exc}") |
| return [heuristic_intent_fallback(email_text) for email_text in email_texts] |
|
|
| intents: list[str] = [] |
|
|
| for email_text in email_texts: |
| try: |
| messages = [ |
| {"role": "system", "content": f"{INTENT_SYSTEM_PROMPT}\n\n{INTENT_REASONING_HINT}"}, |
| {"role": "user", "content": _build_intent_prompt(email_text)}, |
| ] |
| outputs = intent_pipeline( |
| messages, |
| max_new_tokens=96, |
| do_sample=False, |
| ) |
| generated_text = outputs[0].get("generated_text") if outputs else "" |
| sanitized = sanitize_intent_output(_extract_generated_text(generated_text)) |
| if not is_valid_intent_output(sanitized): |
| sanitized = heuristic_intent_fallback(email_text) |
| except Exception as exc: |
| print(f"[SpaceRuntime] Intent generation failed: {exc}") |
| sanitized = heuristic_intent_fallback(email_text) |
| intents.append(sanitized) |
| if torch.cuda.is_available(): |
| torch.cuda.empty_cache() |
|
|
| return intents |
|
|
|
|
| def sanitize_intent_output(text: str) -> str: |
| cleaned = normalize_text(text) |
| cleaned = cleaned.replace("Here's a rewritten version of the email:", "").strip() |
| cleaned = re.sub(r"^output:\s*", "", cleaned, flags=re.IGNORECASE) |
| cleaned = re.sub(r"^response:\s*", "", cleaned, flags=re.IGNORECASE) |
| marker = "The author wants to" |
| lower_cleaned = cleaned.lower() |
|
|
| if lower_cleaned.startswith("the author wants you to "): |
| cleaned = "The author wants to get you to " + cleaned[len("The author wants you to ") :] |
| elif lower_cleaned.startswith("the author wants the recipient to "): |
| cleaned = "The author wants to get the recipient to " + cleaned[len("The author wants the recipient to ") :] |
| elif lower_cleaned.count(marker.lower()) >= 2: |
| cleaned = cleaned[lower_cleaned.rfind(marker.lower()) :] |
| elif marker.lower() in lower_cleaned: |
| start = lower_cleaned.index(marker.lower()) |
| cleaned = cleaned[start:] |
| else: |
| cleaned = f"{marker} {cleaned.lstrip('-* ').strip()}" |
|
|
| cleaned = re.sub(r"^(The author wants to\s+)+", "The author wants to ", cleaned, flags=re.IGNORECASE) |
| cleaned = re.sub(r"The author wants to\s+The author wants to\s+", "The author wants to ", cleaned, flags=re.IGNORECASE) |
| cleaned = re.sub(r"The author wants to\s+The author wants\b", "The author wants", cleaned, flags=re.IGNORECASE) |
| cleaned = cleaned.replace("The author wants to to ", "The author wants to ") |
| sentence_match = re.match(r"^(.*?[.!?])(\s|$)", cleaned) |
| if sentence_match: |
| cleaned = sentence_match.group(1) |
| if not cleaned.endswith("."): |
| cleaned = f"{cleaned.rstrip('.!?')}." |
| return cleaned |
|
|
|
|
| def is_valid_intent_output(intent: str) -> bool: |
| lowered = intent.lower().strip() |
| invalid_fragments = [ |
| "i cannot", |
| "i can't", |
| "i will not", |
| "reverse-intent decompiler", |
| "do not output any other text", |
| "ignore polite formatting", |
| "here is", |
| "here's", |
| "output:", |
| "response:", |
| ] |
| if not lowered.startswith("the author wants to"): |
| return False |
| if any(fragment in lowered for fragment in invalid_fragments): |
| return False |
| if len(lowered.split()) < 8: |
| return False |
| return True |
|
|
|
|
| def heuristic_intent_fallback(email_text: str) -> str: |
| lowered = normalize_text(email_text).lower() |
|
|
| def has_any(phrases: list[str]) -> bool: |
| return any(phrase in lowered for phrase in phrases) |
|
|
| if has_any(["sign in", "log in", "login", "password", "credential", "mfa", "multi-factor", "portal", "vpn"]): |
| action = "get the recipient to submit account credentials through a sign-in flow" |
| elif has_any(["wire transfer", "remittance", "bank details", "beneficiary", "invoice", "payment", "direct deposit"]): |
| action = "get the recipient to move money or change payment instructions" |
| elif has_any(["shared document", "review", "sign", "attachment", "document", "enable content"]): |
| action = "get the recipient to open or sign a document that advances the sender's goal" |
| elif has_any(["package", "parcel", "delivery", "shipping"]): |
| action = "get the recipient to verify delivery details or make a payment to release a shipment" |
| elif has_any(["sale", "discount", "offer", "buy now", "click here"]): |
| action = "drive the recipient to visit a marketing or monetized destination" |
| else: |
| action = "influence the recipient to take a requested action" |
|
|
| if has_any(["urgent", "today", "before cob", "deadline", "locked out", "immediately"]): |
| tactic = "using urgency" |
| elif has_any(["ceo", "finance", "security", "it administration", "payroll", "legal", "executive"]): |
| tactic = "using authority framing" |
| elif has_any(["confidential", "private", "unreachable by phone"]): |
| tactic = "using secrecy" |
| elif has_any(["reminder", "agenda", "meeting", "no action needed", "maintenance"]): |
| tactic = "through routine business process framing" |
| else: |
| tactic = "through persuasive business framing" |
|
|
| return f"The author wants to {action} {tactic}." |
|
|
|
|
| class SpaceHybridClassifier: |
| def __init__(self, artifact_dir: str | Path) -> None: |
| self.artifact_dir = Path(artifact_dir) |
| self.metadata = json.loads((self.artifact_dir / "metadata.json").read_text(encoding="utf-8")) |
| self.embedder = SentenceTransformer(self.metadata.get("embedder_model", "all-MiniLM-L6-v2")) |
|
|
| self.embedding_model = xgb.XGBClassifier() |
| self.embedding_model.load_model(str(self.artifact_dir / "intent_xgboost_model.json")) |
| self.lexical_model = joblib.load(self.artifact_dir / "lexical_logistic_model.joblib") |
| self.lexical_feature_extractor = joblib.load(self.artifact_dir / "lexical_feature_extractor.joblib") |
| self.blend_weight = float(self.metadata.get("blend_weight", 0.5)) |
| self.threshold = float(self.metadata.get("decision_threshold", 0.5)) |
|
|
| def predict_batch(self, raw_email_texts: list[str]) -> list[PredictionTrace]: |
| intents = generate_intents(raw_email_texts) |
| risk_features = build_risk_feature_matrix(raw_email_texts, intents) |
| intent_vectors = self.embedder.encode(intents, show_progress_bar=False) |
| embedding_input = np.hstack([intent_vectors, risk_features]) |
| embedding_probs = self.embedding_model.predict_proba(embedding_input)[:, 1] |
|
|
| lexical_input = self.lexical_feature_extractor.transform(raw_email_texts, intents) |
| lexical_input = sparse.hstack([lexical_input, sparse.csr_matrix(risk_features)], format="csr") |
| lexical_probs = self.lexical_model.predict_proba(lexical_input)[:, 1] |
|
|
| traces: list[PredictionTrace] = [] |
| for raw_email, intent, embedding_probability, lexical_probability in zip( |
| raw_email_texts, |
| intents, |
| embedding_probs, |
| lexical_probs, |
| strict=False, |
| ): |
| policy_probability, policy_flags = extract_policy_signals(raw_email, intent) |
| blended_probability = self.blend_weight * float(embedding_probability) + (1.0 - self.blend_weight) * float(lexical_probability) |
| probability = max(blended_probability, float(policy_probability)) |
| if policy_flags == ["routine_benign"]: |
| probability = min(probability, 0.24) |
| prediction = int(probability >= self.threshold) |
| traces.append( |
| PredictionTrace( |
| prediction=prediction, |
| probability=float(probability), |
| intent_string=intent, |
| embedding_probability=float(embedding_probability), |
| lexical_probability=float(lexical_probability), |
| policy_probability=float(policy_probability), |
| threshold=self.threshold, |
| ) |
| ) |
| return traces |
|
|