DevodG commited on
Commit
f7059e6
·
1 Parent(s): b483d75

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.8: reasons.append("Aggressive urgency detected")
22
- if intent.impersonation > 0.8: reasons.append("Possible high-authority impersonation")
 
 
 
 
 
 
 
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() && activeTab !== 'image') return;
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 className="h-32 border-2 border-dashed border-white/[0.06] rounded-2xl flex flex-col items-center justify-center text-gray-700 group hover:border-indigo-500/30 transition-all cursor-pointer">
160
- <ImageIcon size={24} className="mb-2 group-hover:text-indigo-500 transition-colors" />
161
- <span className="text-[10px] uppercase tracking-widest font-black">Upload Forensic Screenshot</span>
162
- <span className="text-[9px] mt-1 opacity-40">MMSA Emotional Dissonance engine will process.</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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