Spaces:
Running
Running
fix: functionalize guardian layer with image upload and enhanced local heuristics
Browse files
backend/app/services/live_intel_service.py
CHANGED
|
@@ -234,6 +234,18 @@ class LiveIntelService:
|
|
| 234 |
if isinstance(result, list):
|
| 235 |
evidence.extend(result)
|
| 236 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 237 |
# Deduplicate similar evidence entries
|
| 238 |
seen = set()
|
| 239 |
cleaned: List[dict] = []
|
|
|
|
| 234 |
if isinstance(result, list):
|
| 235 |
evidence.extend(result)
|
| 236 |
|
| 237 |
+
# Local Heuristic Evidence (Works without API keys)
|
| 238 |
+
for domain in domains:
|
| 239 |
+
tld = "." + domain.split(".")[-1]
|
| 240 |
+
if tld in [".top", ".xyz", ".buzz", ".live", ".work"]:
|
| 241 |
+
evidence.append({
|
| 242 |
+
"source": "Janus Infrastructure Scan",
|
| 243 |
+
"signal": "risky_tld",
|
| 244 |
+
"value": tld,
|
| 245 |
+
"severity": "medium",
|
| 246 |
+
"explanation": f"Domain uses a risky TLD ({tld}) frequently used in phishing."
|
| 247 |
+
})
|
| 248 |
+
|
| 249 |
# Deduplicate similar evidence entries
|
| 250 |
seen = set()
|
| 251 |
cleaned: List[dict] = []
|
backend/app/services/risk_service.py
CHANGED
|
@@ -18,8 +18,15 @@ class RiskService:
|
|
| 18 |
risk_score += intent.impersonation * 40
|
| 19 |
risk_score += intent.payment * 30
|
| 20 |
|
| 21 |
-
if intent.urgency > 0.
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
# 2. Entity Rules (Depth: Cross-Entity Matching)
|
| 25 |
if entities.upi_ids and entities.domains:
|
|
@@ -53,9 +60,20 @@ class RiskService:
|
|
| 53 |
risk_score += (dissonance * 40)
|
| 54 |
reasons.append("Emotional Dissonance: Content conflicts with emotional tone")
|
| 55 |
|
| 56 |
-
# 5. Optimization: Market Factual Dissonance
|
| 57 |
-
# If brands contains a ticker (mock check)
|
| 58 |
for brand in entities.brands:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
status = market_watcher.get_watchlist_status()
|
| 60 |
match = next((s for s in status if s["symbol"] == brand.upper()), None)
|
| 61 |
if match and match["price"]:
|
|
|
|
| 18 |
risk_score += intent.impersonation * 40
|
| 19 |
risk_score += intent.payment * 30
|
| 20 |
|
| 21 |
+
if intent.urgency > 0.6:
|
| 22 |
+
risk_score += 20
|
| 23 |
+
reasons.append("Aggressive urgency detected (Psychological Trigger)")
|
| 24 |
+
if intent.impersonation > 0.6:
|
| 25 |
+
risk_score += 25
|
| 26 |
+
reasons.append("Possible high-authority impersonation detected")
|
| 27 |
+
if intent.fear > 0.6:
|
| 28 |
+
risk_score += 20
|
| 29 |
+
reasons.append("Fear-based manipulation detected (Coercive Trigger)")
|
| 30 |
|
| 31 |
# 2. Entity Rules (Depth: Cross-Entity Matching)
|
| 32 |
if entities.upi_ids and entities.domains:
|
|
|
|
| 60 |
risk_score += (dissonance * 40)
|
| 61 |
reasons.append("Emotional Dissonance: Content conflicts with emotional tone")
|
| 62 |
|
| 63 |
+
# 5. Optimization: Market & Brand Factual Dissonance
|
|
|
|
| 64 |
for brand in entities.brands:
|
| 65 |
+
# Brand-Domain Mismatch (ZeroTrust Core)
|
| 66 |
+
if entities.domains:
|
| 67 |
+
brand_lower = brand.lower()
|
| 68 |
+
for domain in entities.domains:
|
| 69 |
+
domain_lower = domain.lower()
|
| 70 |
+
# If brand mentioned but domain doesn't match official brand domain
|
| 71 |
+
# Very simple check for now
|
| 72 |
+
if brand_lower in ["sbi", "hdfc", "icici", "axis", "paytm", "amazon", "flipkart"]:
|
| 73 |
+
if brand_lower not in domain_lower:
|
| 74 |
+
risk_score += 40
|
| 75 |
+
reasons.append(f"Brand Mismatch: Claimed brand '{brand}' does not match link destination '{domain}'")
|
| 76 |
+
|
| 77 |
status = market_watcher.get_watchlist_status()
|
| 78 |
match = next((s for s in status if s["symbol"] == brand.upper()), None)
|
| 79 |
if match and match["price"]:
|
frontend/src/components/ScamGuardian.tsx
CHANGED
|
@@ -1,8 +1,8 @@
|
|
| 1 |
'use client';
|
| 2 |
|
| 3 |
-
import { useState, useEffect } from 'react';
|
| 4 |
import { motion, AnimatePresence } from 'framer-motion';
|
| 5 |
-
import { ShieldAlert, Link, Image as ImageIcon, Search, AlertTriangle, CheckCircle, XCircle, Info, Hash, ExternalLink, Brain, Sparkles } from 'lucide-react';
|
| 6 |
import { apiClient, guardianClient } from '@/lib/api';
|
| 7 |
import type { ScamGuardianResponse } from '@/lib/types';
|
| 8 |
import LiveEvidencePanel from './guardian/LiveEvidencePanel';
|
|
@@ -17,6 +17,9 @@ export default function ScamGuardian() {
|
|
| 17 |
const [guardianStatus, setGuardianStatus] = useState<any>(null);
|
| 18 |
const [isSubmittingFeedback, setIsSubmittingFeedback] = useState(false);
|
| 19 |
const [feedbackSubmitted, setFeedbackSubmitted] = useState<string | null>(null);
|
|
|
|
|
|
|
|
|
|
| 20 |
const liveEvents = useGuardianFeed();
|
| 21 |
|
| 22 |
// Load history and status on mount
|
|
@@ -35,7 +38,7 @@ export default function ScamGuardian() {
|
|
| 35 |
};
|
| 36 |
|
| 37 |
const handleAnalyze = async () => {
|
| 38 |
-
if (!inputValue.trim() &&
|
| 39 |
setIsAnalyzing(true);
|
| 40 |
setFeedbackSubmitted(null);
|
| 41 |
try {
|
|
@@ -43,6 +46,11 @@ export default function ScamGuardian() {
|
|
| 43 |
if (activeTab === 'text') payload.text = inputValue;
|
| 44 |
if (activeTab === 'url') payload.url = inputValue;
|
| 45 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
const res = await guardianClient.analyze(payload);
|
| 47 |
setResult(res as unknown as ScamGuardianResponse);
|
| 48 |
fetchHistory(); // Refresh history
|
|
@@ -53,6 +61,23 @@ export default function ScamGuardian() {
|
|
| 53 |
}
|
| 54 |
};
|
| 55 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
const handleFeedback = async (isScam: boolean) => {
|
| 57 |
if (!result) return;
|
| 58 |
setIsSubmittingFeedback(true);
|
|
@@ -156,10 +181,35 @@ export default function ScamGuardian() {
|
|
| 156 |
/>
|
| 157 |
)}
|
| 158 |
{activeTab === 'image' && (
|
| 159 |
-
<div
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
</div>
|
| 164 |
)}
|
| 165 |
|
|
|
|
| 1 |
'use client';
|
| 2 |
|
| 3 |
+
import { useState, useEffect, useRef } from 'react';
|
| 4 |
import { motion, AnimatePresence } from 'framer-motion';
|
| 5 |
+
import { ShieldAlert, Link, Image as ImageIcon, Search, AlertTriangle, CheckCircle, XCircle, Info, Hash, ExternalLink, Brain, Sparkles, Upload } from 'lucide-react';
|
| 6 |
import { apiClient, guardianClient } from '@/lib/api';
|
| 7 |
import type { ScamGuardianResponse } from '@/lib/types';
|
| 8 |
import LiveEvidencePanel from './guardian/LiveEvidencePanel';
|
|
|
|
| 17 |
const [guardianStatus, setGuardianStatus] = useState<any>(null);
|
| 18 |
const [isSubmittingFeedback, setIsSubmittingFeedback] = useState(false);
|
| 19 |
const [feedbackSubmitted, setFeedbackSubmitted] = useState<string | null>(null);
|
| 20 |
+
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
| 21 |
+
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
| 22 |
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
| 23 |
const liveEvents = useGuardianFeed();
|
| 24 |
|
| 25 |
// Load history and status on mount
|
|
|
|
| 38 |
};
|
| 39 |
|
| 40 |
const handleAnalyze = async () => {
|
| 41 |
+
if (!inputValue.trim() && !selectedFile) return;
|
| 42 |
setIsAnalyzing(true);
|
| 43 |
setFeedbackSubmitted(null);
|
| 44 |
try {
|
|
|
|
| 46 |
if (activeTab === 'text') payload.text = inputValue;
|
| 47 |
if (activeTab === 'url') payload.url = inputValue;
|
| 48 |
|
| 49 |
+
if (activeTab === 'image' && selectedFile) {
|
| 50 |
+
const base64 = await fileToBase64(selectedFile);
|
| 51 |
+
payload.image_base64 = base64;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
const res = await guardianClient.analyze(payload);
|
| 55 |
setResult(res as unknown as ScamGuardianResponse);
|
| 56 |
fetchHistory(); // Refresh history
|
|
|
|
| 61 |
}
|
| 62 |
};
|
| 63 |
|
| 64 |
+
const fileToBase64 = (file: File): Promise<string> => {
|
| 65 |
+
return new Promise((resolve, reject) => {
|
| 66 |
+
const reader = new FileReader();
|
| 67 |
+
reader.readAsDataURL(file);
|
| 68 |
+
reader.onload = () => resolve(reader.result as string);
|
| 69 |
+
reader.onerror = (error) => reject(error);
|
| 70 |
+
});
|
| 71 |
+
};
|
| 72 |
+
|
| 73 |
+
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
| 74 |
+
const file = e.target.files?.[0];
|
| 75 |
+
if (file) {
|
| 76 |
+
setSelectedFile(file);
|
| 77 |
+
setPreviewUrl(URL.createObjectURL(file));
|
| 78 |
+
}
|
| 79 |
+
};
|
| 80 |
+
|
| 81 |
const handleFeedback = async (isScam: boolean) => {
|
| 82 |
if (!result) return;
|
| 83 |
setIsSubmittingFeedback(true);
|
|
|
|
| 181 |
/>
|
| 182 |
)}
|
| 183 |
{activeTab === 'image' && (
|
| 184 |
+
<div
|
| 185 |
+
onClick={() => fileInputRef.current?.click()}
|
| 186 |
+
className={`h-32 border-2 border-dashed rounded-2xl flex flex-col items-center justify-center transition-all cursor-pointer relative overflow-hidden group ${
|
| 187 |
+
previewUrl ? 'border-indigo-500/50 bg-indigo-500/5' : 'border-white/[0.06] text-gray-700 hover:border-indigo-500/30'
|
| 188 |
+
}`}
|
| 189 |
+
>
|
| 190 |
+
<input
|
| 191 |
+
type="file"
|
| 192 |
+
ref={fileInputRef}
|
| 193 |
+
onChange={handleFileChange}
|
| 194 |
+
accept="image/*"
|
| 195 |
+
className="hidden"
|
| 196 |
+
/>
|
| 197 |
+
{previewUrl ? (
|
| 198 |
+
<>
|
| 199 |
+
<img src={previewUrl} alt="Preview" className="absolute inset-0 w-full h-full object-cover opacity-20" />
|
| 200 |
+
<div className="relative z-10 flex flex-col items-center">
|
| 201 |
+
<Upload size={20} className="mb-1 text-indigo-400" />
|
| 202 |
+
<span className="text-[10px] uppercase font-black text-indigo-400">Change Image</span>
|
| 203 |
+
<span className="text-[9px] text-gray-500 mt-0.5">{selectedFile?.name}</span>
|
| 204 |
+
</div>
|
| 205 |
+
</>
|
| 206 |
+
) : (
|
| 207 |
+
<>
|
| 208 |
+
<ImageIcon size={24} className="mb-2 group-hover:text-indigo-500 transition-colors" />
|
| 209 |
+
<span className="text-[10px] uppercase tracking-widest font-black">Upload Forensic Screenshot</span>
|
| 210 |
+
<span className="text-[9px] mt-1 opacity-40">MMSA Emotional Dissonance engine will process.</span>
|
| 211 |
+
</>
|
| 212 |
+
)}
|
| 213 |
</div>
|
| 214 |
)}
|
| 215 |
|