|
12 | 12 | from datetime import datetime |
13 | 13 | from typing import Optional |
14 | 14 | import redis |
| 15 | +import requests |
15 | 16 | from behavior import BehaviorEngine |
16 | 17 | from semantic import OpenRouterSimilarityDetector |
17 | 18 | from fusion import FusionEngine |
|
22 | 23 | EVENTS_CHANNEL = "events" |
23 | 24 | ALERT_THRESHOLD = 0.7 |
24 | 25 | PERFORMANCE_TARGET_MS = 500 |
| 26 | +GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") |
25 | 27 |
|
26 | 28 |
|
27 | 29 | class ShadowGuardWorker: |
@@ -144,18 +146,48 @@ def _format_alert(self, result: dict, log_data: dict, processing_time_ms: float) |
144 | 146 | """ |
145 | 147 | return alert |
146 | 148 |
|
| 149 | + def _generate_ai_explanation(self, domain: str, url: str, category: str) -> str: |
| 150 | + """Generate a concise AI explanation using Gemini API.""" |
| 151 | + if not GEMINI_API_KEY: |
| 152 | + return f"Detected access to {category} service" |
| 153 | + |
| 154 | + try: |
| 155 | + prompt = ( |
| 156 | + f"You are a security analyst. Explain why accessing {domain} ({url}) " |
| 157 | + f"is a security risk in 1 short sentence. Category: {category}." |
| 158 | + ) |
| 159 | + |
| 160 | + response = requests.post( |
| 161 | + f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={GEMINI_API_KEY}", |
| 162 | + json={"contents": [{"parts": [{"text": prompt}]}]}, |
| 163 | + timeout=5 |
| 164 | + ) |
| 165 | + |
| 166 | + if response.status_code == 200: |
| 167 | + text = response.json()["candidates"][0]["content"]["parts"][0]["text"] |
| 168 | + return text.strip() |
| 169 | + else: |
| 170 | + print(f"[WARN] Gemini API returned {response.status_code}: {response.text}") |
| 171 | + except Exception as e: |
| 172 | + print(f"[WARN] Gemini API failed: {e}") |
| 173 | + |
| 174 | + return f"Detected access to {category} service" |
| 175 | + |
147 | 176 | def _save_alert_to_redis(self, result: dict): |
148 | 177 | """Save alert to Redis for backend consumption.""" |
149 | 178 | try: |
150 | | - # Handle override vs normal analysis |
| 179 | + # Determine category |
151 | 180 | if result.get("override"): |
152 | | - # Blacklisted/Whitelisted domain - use override data |
153 | 181 | category = "Blacklisted" if result["final_risk_score"] > 0.5 else "Whitelisted" |
154 | | - ai_message = result.get("override_reason", "No explanation") |
155 | 182 | else: |
156 | | - # Normal analysis - use semantic data |
157 | 183 | category = result.get("semantic_analysis", {}).get("top_category", "unknown") |
158 | | - ai_message = result.get("semantic_analysis", {}).get("explanation", "No analysis available") |
| 184 | + |
| 185 | + # Always generate AI explanation |
| 186 | + ai_message = self._generate_ai_explanation( |
| 187 | + domain=result["domain"], |
| 188 | + url=result.get("url", ""), |
| 189 | + category=category |
| 190 | + ) |
159 | 191 |
|
160 | 192 | # Format alert for frontend |
161 | 193 | alert = { |
|
0 commit comments