Che237 commited on
Commit
91af9cd
·
0 Parent(s):

Wave 2: Context-grounded security chatbot — /api/v2/chat upgrade

Browse files
Files changed (4) hide show
  1. Dockerfile +38 -0
  2. README.md +45 -0
  3. app.py +1945 -0
  4. requirements.txt +45 -0
Dockerfile ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use Python 3.11 to avoid the gradio_client Python 3.13 bug
2
+ FROM python:3.11-slim
3
+
4
+ WORKDIR /app
5
+
6
+ # Install system dependencies
7
+ RUN apt-get update && apt-get install -y \
8
+ git \
9
+ git-lfs \
10
+ ffmpeg \
11
+ libsm6 \
12
+ libxext6 \
13
+ && rm -rf /var/lib/apt/lists/* \
14
+ && git lfs install
15
+
16
+ # Create user for HF Spaces
17
+ RUN useradd -m -u 1000 user
18
+ USER user
19
+ ENV HOME=/home/user \
20
+ PATH=/home/user/.local/bin:$PATH
21
+
22
+ WORKDIR $HOME/app
23
+
24
+ # Copy requirements and install dependencies
25
+ COPY --chown=user requirements.txt .
26
+ RUN pip install --no-cache-dir --upgrade pip && \
27
+ pip install --no-cache-dir -r requirements.txt && \
28
+ pip install --no-cache-dir ipykernel && \
29
+ python -m ipykernel install --user --name python3 --display-name "Python 3"
30
+
31
+ # Copy application code
32
+ COPY --chown=user . .
33
+
34
+ # Expose port
35
+ EXPOSE 7860
36
+
37
+ # Run the application via uvicorn (FastAPI + Gradio mounted)
38
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
README.md ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: CyberForge AI
3
+ emoji: 🔐
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: docker
7
+ app_file: app.py
8
+ pinned: false
9
+ license: mit
10
+ short_description: CyberForge AI - ML Training & Inference Platform
11
+ ---
12
+
13
+ # 🔐 CyberForge AI - ML Training & Inference Platform
14
+
15
+ Gradio UI for notebook execution, model training, and inference + FastAPI REST API for the Heroku backend.
16
+
17
+ ## REST API Endpoints (for backend mlService.js)
18
+
19
+ | Endpoint | Method | Description |
20
+ |----------|--------|-------------|
21
+ | `/health` | GET | Health check |
22
+ | `/analyze` | POST | AI chat (Gemini) |
23
+ | `/analyze-url` | POST | URL threat analysis |
24
+ | `/scan-threats` | POST | Threat scanning |
25
+ | `/api/insights/generate` | POST | AI insights |
26
+ | `/api/models/predict` | POST | ML model prediction |
27
+ | `/models` | GET | List available models |
28
+ | `/api/analysis/network` | POST | Network traffic analysis |
29
+ | `/api/ai/execute-task` | POST | AI task execution |
30
+
31
+ ## Environment Secrets
32
+
33
+ Set these in your Space settings → Secrets:
34
+
35
+ - `GEMINI_API_KEY` - Google Gemini API key
36
+ - `HF_TOKEN` - HuggingFace token (for model downloads)
37
+ - `HF_MODEL_REPO` - Model repository (default: `Che237/cyberforge-models`)
38
+
39
+ ## Backend Integration
40
+
41
+ Your Heroku backend connects to this Space:
42
+
43
+ ```bash
44
+ heroku config:set AI_SERVICE_URL=https://che237-cyberforge.hf.space -a cyberforge
45
+ ```
app.py ADDED
@@ -0,0 +1,1945 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ CyberForge AI - ML Training & Inference Platform
3
+ Hugging Face Spaces deployment with:
4
+ 1) Gradio UI for notebook execution, training, and inference
5
+ 2) FastAPI REST endpoints for the Heroku backend (mlService.js)
6
+ """
7
+
8
+ import gradio as gr
9
+ import pandas as pd
10
+ import numpy as np
11
+ import json
12
+ import os
13
+ import subprocess
14
+ import sys
15
+ from pathlib import Path
16
+ from datetime import datetime
17
+ import logging
18
+ from typing import Dict, List, Any, Optional, Tuple
19
+ from urllib.parse import urlparse
20
+
21
+ # ML Libraries
22
+ from sklearn.model_selection import train_test_split, cross_val_score
23
+ from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier, IsolationForest
24
+ from sklearn.linear_model import LogisticRegression
25
+ from sklearn.preprocessing import StandardScaler, LabelEncoder
26
+ from sklearn.metrics import classification_report, confusion_matrix, accuracy_score, f1_score
27
+ import joblib
28
+
29
+ # Hugging Face Hub
30
+ from huggingface_hub import HfApi, hf_hub_download, upload_file
31
+
32
+ # FastAPI for REST endpoints
33
+ from fastapi import FastAPI, Request
34
+ from fastapi.middleware.cors import CORSMiddleware
35
+ from fastapi.responses import JSONResponse
36
+
37
+ # Gemini AI (new SDK: google-genai)
38
+ try:
39
+ from google import genai
40
+ GEMINI_AVAILABLE = True
41
+ except ImportError:
42
+ GEMINI_AVAILABLE = False
43
+
44
+ logging.basicConfig(level=logging.INFO)
45
+ logger = logging.getLogger(__name__)
46
+
47
+ # ============================================================================
48
+ # CONFIGURATION
49
+ # ============================================================================
50
+
51
+ APP_DIR = Path(__file__).parent.absolute()
52
+ MODELS_DIR = APP_DIR / "trained_models"
53
+ MODELS_DIR.mkdir(exist_ok=True)
54
+ DATASETS_DIR = APP_DIR / "datasets"
55
+ DATASETS_DIR.mkdir(exist_ok=True)
56
+ NOTEBOOKS_DIR = APP_DIR / "notebooks"
57
+ KNOWLEDGE_BASE_DIR = APP_DIR / "knowledge_base"
58
+ KNOWLEDGE_BASE_DIR.mkdir(exist_ok=True)
59
+ TRAINING_DATA_DIR = APP_DIR / "training_data"
60
+ TRAINING_DATA_DIR.mkdir(exist_ok=True)
61
+
62
+ # Environment
63
+ GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY", "")
64
+ GEMINI_MODEL = os.environ.get("GEMINI_MODEL", "gemini-2.5-flash")
65
+ HF_TOKEN = os.environ.get("HF_TOKEN", "")
66
+ HF_MODEL_REPO = os.environ.get("HF_MODEL_REPO", "Che237/cyberforge-models")
67
+
68
+ logger.info(f"APP_DIR: {APP_DIR}")
69
+ logger.info(f"NOTEBOOKS_DIR: {NOTEBOOKS_DIR}")
70
+ logger.info(f"NOTEBOOKS_DIR exists: {NOTEBOOKS_DIR.exists()}")
71
+
72
+ # Model types available for training
73
+ MODEL_TYPES = {
74
+ "Random Forest": RandomForestClassifier,
75
+ "Gradient Boosting": GradientBoostingClassifier,
76
+ "Logistic Regression": LogisticRegression,
77
+ "Isolation Forest (Anomaly)": IsolationForest,
78
+ }
79
+
80
+ SECURITY_TASKS = [
81
+ "Malware Detection", "Phishing Detection", "Network Intrusion Detection",
82
+ "Anomaly Detection", "Botnet Detection", "Web Attack Detection",
83
+ "Spam Detection", "Vulnerability Assessment", "DNS Tunneling Detection",
84
+ "Cryptomining Detection",
85
+ ]
86
+
87
+
88
+ # ============================================================================
89
+ # GEMINI SERVICE (for REST API)
90
+ # ============================================================================
91
+
92
+ class GeminiService:
93
+ """Google Gemini AI for cybersecurity chat and analysis"""
94
+
95
+ SYSTEM_PROMPT = """You are CyberForge AI, an advanced cybersecurity expert. You specialize in:
96
+ - Real-time threat detection and analysis
97
+ - Malware and phishing identification
98
+ - Network security assessment
99
+ - Browser security monitoring
100
+ - Risk assessment and mitigation
101
+
102
+ When analyzing security queries, provide:
103
+ 1. Risk Level (Critical/High/Medium/Low)
104
+ 2. Threat Types identified
105
+ 3. Confidence Score (0.0-1.0)
106
+ 4. Detailed technical analysis
107
+ 5. Specific actionable recommendations
108
+
109
+ Always be precise, professional, and actionable."""
110
+
111
+ def __init__(self):
112
+ self.client = None
113
+ self.ready = False
114
+ self.custom_knowledge = {}
115
+ self.training_examples = []
116
+
117
+ def initialize(self):
118
+ if not GEMINI_AVAILABLE:
119
+ logger.warning("google-genai not installed, Gemini unavailable")
120
+ return
121
+ if not GEMINI_API_KEY:
122
+ logger.warning("GEMINI_API_KEY not set, Gemini unavailable")
123
+ return
124
+ try:
125
+ self.client = genai.Client(api_key=GEMINI_API_KEY)
126
+ resp = self.client.models.generate_content(
127
+ model=GEMINI_MODEL,
128
+ contents="Test. Respond with OK."
129
+ )
130
+ if resp.text:
131
+ self.ready = True
132
+ logger.info(f"✅ Gemini initialized (model: {GEMINI_MODEL})")
133
+ self._load_knowledge()
134
+ except Exception as e:
135
+ logger.error(f"❌ Gemini init failed: {e}")
136
+ self.ready = False
137
+
138
+ def _load_knowledge(self):
139
+ try:
140
+ for f in KNOWLEDGE_BASE_DIR.glob("*.json"):
141
+ with open(f) as fh:
142
+ self.custom_knowledge[f.stem] = json.load(fh)
143
+ logger.info(f"Loaded {len(self.custom_knowledge)} knowledge files")
144
+ except Exception as e:
145
+ logger.warning(f"Knowledge load error: {e}")
146
+ try:
147
+ for f in TRAINING_DATA_DIR.glob("*.json"):
148
+ with open(f) as fh:
149
+ data = json.load(fh)
150
+ if isinstance(data, list):
151
+ self.training_examples.extend(data)
152
+ else:
153
+ self.training_examples.append(data)
154
+ logger.info(f"Loaded {len(self.training_examples)} training examples")
155
+ except Exception as e:
156
+ logger.warning(f"Training data load error: {e}")
157
+
158
+ def analyze(self, query: str, context: Dict = None, history: List = None) -> Dict:
159
+ if not self.ready:
160
+ return self._fallback(query)
161
+ try:
162
+ knowledge_str = json.dumps(self.custom_knowledge, indent=1)[:2000] if self.custom_knowledge else "None"
163
+ examples_str = "\n".join(
164
+ f"Q: {ex.get('input','')}\nA: {ex.get('output','')}"
165
+ for ex in self.training_examples[-3:]
166
+ ) if self.training_examples else "None"
167
+ context_str = f"\nCONTEXT:\n{json.dumps(context, indent=1)[:3000]}\n" if context else ""
168
+
169
+ prompt = f"""{self.SYSTEM_PROMPT}
170
+
171
+ KNOWLEDGE BASE (summary):
172
+ {knowledge_str}
173
+
174
+ TRAINING EXAMPLES:
175
+ {examples_str}
176
+ {context_str}
177
+ USER QUERY:
178
+ {query}
179
+
180
+ Provide a comprehensive cybersecurity analysis:"""
181
+
182
+ response = self.client.models.generate_content(
183
+ model=GEMINI_MODEL,
184
+ contents=prompt,
185
+ config={"temperature": 0.3, "max_output_tokens": 2048},
186
+ )
187
+ text = response.text if response.text else "No response generated"
188
+ text_lower = text.lower()
189
+ if "critical" in text_lower:
190
+ risk_level, risk_score = "Critical", 9.0
191
+ elif "high" in text_lower:
192
+ risk_level, risk_score = "High", 7.0
193
+ elif "medium" in text_lower:
194
+ risk_level, risk_score = "Medium", 5.0
195
+ else:
196
+ risk_level, risk_score = "Low", 3.0
197
+
198
+ return {
199
+ "response": text,
200
+ "confidence": 0.85,
201
+ "risk_level": risk_level,
202
+ "risk_score": risk_score,
203
+ "insights": [f"Analysis performed by Gemini ({GEMINI_MODEL})"],
204
+ "recommendations": [],
205
+ "model_used": GEMINI_MODEL,
206
+ "timestamp": datetime.utcnow().isoformat(),
207
+ }
208
+ except Exception as e:
209
+ logger.error(f"Gemini analysis error: {e}")
210
+ return self._fallback(query)
211
+
212
+ def _fallback(self, query: str) -> Dict:
213
+ """ML-powered analysis when Gemini is unavailable"""
214
+ import re
215
+ global ml_loader
216
+
217
+ url_pattern = re.compile(r'https?://[^\s"\'<>]+')
218
+ urls = url_pattern.findall(query)
219
+
220
+ risk_level = "Unknown"
221
+ risk_score = 0.0
222
+ insights = []
223
+ response_lines = ["## CyberForge ML Security Analysis\n"]
224
+
225
+ try:
226
+ loader = ml_loader
227
+ n_loaded = len(loader.models)
228
+ except Exception:
229
+ loader = None
230
+ n_loaded = 0
231
+
232
+ if urls and loader and n_loaded > 0:
233
+ threat_models_fired = []
234
+ all_scores = []
235
+
236
+ for url in urls[:3]:
237
+ features = extract_url_features(url)
238
+ url_threats = []
239
+ url_scores = []
240
+
241
+ for model_name in ["phishing_detection", "malware_detection", "web_attack_detection"]:
242
+ pred = loader.predict(model_name, features)
243
+ if pred.get("prediction", 0) == 1:
244
+ label = model_name.replace("_detection", "").replace("_", " ").title()
245
+ url_threats.append(label)
246
+ url_scores.append(pred.get("confidence", 0.5))
247
+ threat_models_fired.append(model_name)
248
+
249
+ avg = sum(url_scores) / len(url_scores) if url_scores else 0.15
250
+ all_scores.append(avg)
251
+ lvl = "HIGH" if avg > 0.6 else "MEDIUM" if avg > 0.35 else "LOW"
252
+ threats_str = ", ".join(url_threats) if url_threats else "None detected"
253
+ display_url = url if len(url) <= 70 else url[:67] + "..."
254
+ response_lines.append(f"**URL:** `{display_url}`")
255
+ response_lines.append(f"- Risk: **{lvl}** | Threats: {threats_str} | Score: {avg:.0%}\n")
256
+
257
+ overall = sum(all_scores) / len(all_scores) if all_scores else 0.2
258
+ if overall > 0.65:
259
+ risk_level, risk_score = "High", 7.5
260
+ elif overall > 0.4:
261
+ risk_level, risk_score = "Medium", 5.0
262
+ else:
263
+ risk_level, risk_score = "Low", 2.5
264
+
265
+ insights = [f"{n_loaded} ML models active"] + [
266
+ f"Threat model triggered: {m.replace('_detection', '').replace('_', ' ').title()}"
267
+ for m in set(threat_models_fired)
268
+ ]
269
+
270
+ if risk_level == "High":
271
+ response_lines.append("### Recommendation\n⚠️ **Block immediately.** Phishing or malware indicators detected — do not visit this URL.")
272
+ elif risk_level == "Medium":
273
+ response_lines.append("### Recommendation\n⚡ **Exercise caution.** Validate with additional threat intelligence before accessing.")
274
+ else:
275
+ response_lines.append("### Recommendation\n✅ **URL appears structurally safe** based on ML analysis.")
276
+
277
+ else:
278
+ query_lower = query.lower()
279
+ malware_kws = ["malware", "virus", "ransomware", "trojan", "spyware", "backdoor", "worm"]
280
+ phishing_kws = ["phishing", "credential", "fake login", "spoof", "scam", "social engineering"]
281
+ safe_kws = ["safe", "legitimate", "trusted", "secure", "verify"]
282
+
283
+ if any(k in query_lower for k in malware_kws):
284
+ risk_level, risk_score = "High", 7.0
285
+ response_lines.append(
286
+ "**Malware indicators detected in your query.**\n\n"
287
+ "Recommended actions:\n"
288
+ "- Isolate the affected system immediately\n"
289
+ "- Run a full endpoint detection scan\n"
290
+ "- Review recently installed software and browser extensions\n"
291
+ "- Check startup processes and scheduled tasks for persistence\n"
292
+ "- Rotate credentials if any exposure is suspected"
293
+ )
294
+ elif any(k in query_lower for k in phishing_kws):
295
+ risk_level, risk_score = "High", 7.0
296
+ response_lines.append(
297
+ "**Phishing threat indicators detected.**\n\n"
298
+ "Recommended actions:\n"
299
+ "- Do not submit credentials to the suspected site\n"
300
+ "- Verify sender identity through a secondary channel\n"
301
+ "- Report to your IT security team immediately\n"
302
+ "- Enable MFA on all accounts that may be affected\n"
303
+ "- Review email headers for spoofing indicators"
304
+ )
305
+ elif any(k in query_lower for k in safe_kws):
306
+ risk_level, risk_score = "Low", 2.0
307
+ response_lines.append("No immediate threat indicators detected. Continue standard monitoring procedures.")
308
+ else:
309
+ response_lines.append(
310
+ f"CyberForge ML is operational with **{n_loaded}/4 models** loaded.\n\n"
311
+ "For best results, include a URL or specific threat indicators in your query.\n\n"
312
+ "**Available analysis capabilities:**\n"
313
+ "- URL threat analysis (phishing, malware, web attacks)\n"
314
+ "- Network anomaly detection\n"
315
+ "- Real-time threat event monitoring\n\n"
316
+ "*AI chat (Gemini) is currently unavailable. Provide a URL for full ML-based analysis.*"
317
+ )
318
+ insights = [f"{n_loaded} ML models active"]
319
+
320
+ response_lines.append("\n---\n*Powered by CyberForge ML models — Gemini AI offline.*")
321
+
322
+ return {
323
+ "response": "\n".join(response_lines),
324
+ "confidence": 0.65 if urls else 0.4,
325
+ "risk_level": risk_level,
326
+ "risk_score": risk_score,
327
+ "insights": insights,
328
+ "recommendations": [],
329
+ "model_used": "cyberforge-ml-fallback",
330
+ "timestamp": datetime.utcnow().isoformat(),
331
+ }
332
+
333
+
334
+ # Singleton
335
+ gemini_service = GeminiService()
336
+
337
+
338
+ # ============================================================================
339
+ # ML MODEL LOADER (loads .pkl from HF Hub for REST predictions)
340
+ # ============================================================================
341
+
342
+ class MLModelLoader:
343
+ MODEL_NAMES = [
344
+ "phishing_detection", "malware_detection",
345
+ "anomaly_detection", "web_attack_detection",
346
+ ]
347
+
348
+ def __init__(self):
349
+ self.models: Dict[str, Any] = {}
350
+ self.scalers: Dict[str, Any] = {}
351
+ self.ready = False
352
+
353
+ def initialize(self):
354
+ loaded = 0
355
+ # All directories where models might exist (notebooks save to ../models)
356
+ search_dirs = [
357
+ MODELS_DIR, # trained_models/
358
+ APP_DIR / "models", # models/ (where notebooks output)
359
+ APP_DIR.parent / "models", # one level up fallback
360
+ ]
361
+
362
+ for name in self.MODEL_NAMES:
363
+ if name in self.models:
364
+ continue
365
+ try:
366
+ # 1. Try HuggingFace Hub first
367
+ model_file = f"{name}/best_model.pkl"
368
+ scaler_file = f"{name}/scaler.pkl"
369
+ try:
370
+ model_path = hf_hub_download(
371
+ repo_id=HF_MODEL_REPO, filename=model_file,
372
+ token=HF_TOKEN or None, cache_dir=str(MODELS_DIR),
373
+ )
374
+ scaler_path = hf_hub_download(
375
+ repo_id=HF_MODEL_REPO, filename=scaler_file,
376
+ token=HF_TOKEN or None, cache_dir=str(MODELS_DIR),
377
+ )
378
+ self.models[name] = joblib.load(model_path)
379
+ self.scalers[name] = joblib.load(scaler_path)
380
+ loaded += 1
381
+ logger.info(f"✅ Loaded model from Hub: {name}")
382
+ continue
383
+ except Exception:
384
+ pass
385
+
386
+ # 2. Try all local search directories
387
+ for sdir in search_dirs:
388
+ if name in self.models:
389
+ break
390
+ for model_fname in [f"{name}/best_model.pkl", f"{name}/model.pkl", f"{name}_model.pkl"]:
391
+ candidate = sdir / model_fname
392
+ if candidate.exists():
393
+ self.models[name] = joblib.load(candidate)
394
+ # Try to find matching scaler
395
+ for scaler_fname in [f"{name}/scaler.pkl", f"{name}_scaler.pkl"]:
396
+ sc = sdir / scaler_fname
397
+ if sc.exists():
398
+ self.scalers[name] = joblib.load(sc)
399
+ break
400
+ loaded += 1
401
+ logger.info(f"✅ Loaded model from {sdir.name}/{model_fname}: {name}")
402
+ break
403
+
404
+ except Exception as e:
405
+ logger.warning(f"Error loading model {name}: {e}")
406
+
407
+ # Sweep all search dirs for any .pkl files not yet loaded
408
+ for sdir in search_dirs:
409
+ if not sdir.exists():
410
+ continue
411
+ for pkl in sdir.glob("*.pkl"):
412
+ stem = pkl.stem.replace("_model", "").replace("_best", "")
413
+ if stem not in self.models:
414
+ try:
415
+ self.models[stem] = joblib.load(pkl)
416
+ loaded += 1
417
+ logger.info(f"✅ Loaded model sweep: {stem} from {sdir.name}")
418
+ except Exception:
419
+ pass
420
+
421
+ self.ready = loaded > 0
422
+ logger.info(f"ML Models: {loaded} loaded — {list(self.models.keys())}")
423
+
424
+ def predict(self, model_name: str, features: Dict) -> Dict:
425
+ if model_name not in self.models:
426
+ return self._heuristic_predict(model_name, features)
427
+ try:
428
+ model = self.models[model_name]
429
+ scaler = self.scalers.get(model_name)
430
+ X = np.array([list(features.values())])
431
+ if scaler:
432
+ X = scaler.transform(X)
433
+ prediction = int(model.predict(X)[0])
434
+ confidence = 0.5
435
+ probabilities = {}
436
+ if hasattr(model, "predict_proba"):
437
+ proba = model.predict_proba(X)[0]
438
+ confidence = float(max(proba))
439
+ probabilities = {str(i): round(float(p), 4) for i, p in enumerate(proba)}
440
+ # Derive a consistent threat_score: probability of class-1 (threat)
441
+ # so that the /analyze-url aggregation has a uniform field across all models
442
+ if "1" in probabilities:
443
+ threat_score = probabilities["1"]
444
+ elif prediction == 1:
445
+ threat_score = confidence
446
+ else:
447
+ threat_score = 1.0 - confidence
448
+ return {
449
+ "model": model_name,
450
+ "prediction": prediction,
451
+ "prediction_label": "threat" if prediction == 1 else "benign",
452
+ "confidence": round(confidence, 4),
453
+ "threat_score": round(threat_score, 4),
454
+ "probabilities": probabilities,
455
+ "inference_source": "ml_model",
456
+ "timestamp": datetime.utcnow().isoformat(),
457
+ }
458
+ except Exception as e:
459
+ logger.error(f"Prediction error {model_name}: {e}")
460
+ return self._heuristic_predict(model_name, features)
461
+
462
+ def feature_importance(self, model_name: str) -> Dict:
463
+ """Return feature importances for tree-based models (explainability)."""
464
+ feature_names = [
465
+ "url_length", "hostname_length", "path_length", "is_https",
466
+ "has_ip_address", "has_suspicious_tld", "subdomain_count",
467
+ "has_port", "query_params_count", "has_at_symbol",
468
+ "has_double_slash", "special_char_count",
469
+ ]
470
+ if model_name not in self.models:
471
+ return {"error": f"Model '{model_name}' not loaded", "available": list(self.models.keys())}
472
+ model = self.models[model_name]
473
+ try:
474
+ if hasattr(model, "feature_importances_"):
475
+ importances = model.feature_importances_
476
+ pairs = sorted(
477
+ zip(feature_names, [round(float(v), 4) for v in importances]),
478
+ key=lambda x: x[1], reverse=True,
479
+ )
480
+ return {
481
+ "model": model_name,
482
+ "method": "gini_importance",
483
+ "feature_importances": dict(pairs),
484
+ "top_features": [p[0] for p in pairs[:5]],
485
+ }
486
+ elif hasattr(model, "coef_"):
487
+ coefs = model.coef_[0] if model.coef_.ndim > 1 else model.coef_
488
+ pairs = sorted(
489
+ zip(feature_names, [round(float(v), 4) for v in coefs]),
490
+ key=lambda x: abs(x[1]), reverse=True,
491
+ )
492
+ return {
493
+ "model": model_name,
494
+ "method": "logistic_coefficients",
495
+ "feature_importances": dict(pairs),
496
+ "top_features": [p[0] for p in pairs[:5]],
497
+ }
498
+ else:
499
+ return {
500
+ "model": model_name,
501
+ "method": "not_available",
502
+ "note": f"Model type {type(model).__name__} does not expose feature importances",
503
+ }
504
+ except Exception as e:
505
+ return {"model": model_name, "error": str(e)}
506
+
507
+ def _heuristic_predict(self, model_name: str, features: Dict) -> Dict:
508
+ score = 0.0
509
+ reasons = []
510
+ is_https = features.get("is_https", features.get("has_https", 1))
511
+ has_ip = features.get("has_ip_address", 0)
512
+ suspicious_tld = features.get("has_suspicious_tld", 0)
513
+ url_length = features.get("url_length", 0)
514
+ special_chars = features.get("special_char_count", 0)
515
+ if not is_https:
516
+ score += 0.3; reasons.append("No HTTPS")
517
+ if has_ip:
518
+ score += 0.25; reasons.append("IP address in URL")
519
+ if suspicious_tld:
520
+ score += 0.2; reasons.append("Suspicious TLD")
521
+ if url_length > 100:
522
+ score += 0.15; reasons.append("Very long URL")
523
+ if special_chars > 10:
524
+ score += 0.1; reasons.append("Many special characters")
525
+ is_threat = score >= 0.5
526
+ return {
527
+ "model": model_name,
528
+ "prediction": 1 if is_threat else 0,
529
+ "prediction_label": "threat" if is_threat else "benign",
530
+ "confidence": min(score + 0.3, 0.95) if is_threat else max(0.6, 1.0 - score),
531
+ "threat_score": score,
532
+ "reasons": reasons,
533
+ "inference_source": "heuristic",
534
+ "timestamp": datetime.utcnow().isoformat(),
535
+ }
536
+
537
+
538
+ ml_loader = MLModelLoader()
539
+
540
+
541
+ # ============================================================================
542
+ # URL FEATURE EXTRACTION
543
+ # ============================================================================
544
+
545
+ def extract_url_features(url: str) -> Dict:
546
+ parsed = urlparse(url)
547
+ hostname = parsed.hostname or ""
548
+ return {
549
+ "url_length": len(url),
550
+ "hostname_length": len(hostname),
551
+ "path_length": len(parsed.path or ""),
552
+ "is_https": 1 if parsed.scheme == "https" else 0,
553
+ "has_ip_address": 1 if all(p.isdigit() for p in hostname.split(".")) and len(hostname.split(".")) == 4 else 0,
554
+ "has_suspicious_tld": 1 if any(hostname.endswith(t) for t in [".xyz", ".tk", ".ml", ".ga", ".cf", ".top", ".buzz"]) else 0,
555
+ "subdomain_count": max(0, len(hostname.split(".")) - 2),
556
+ "has_port": 1 if parsed.port else 0,
557
+ "query_params_count": len(parsed.query.split("&")) if parsed.query else 0,
558
+ "has_at_symbol": 1 if "@" in url else 0,
559
+ "has_double_slash": 1 if "//" in (parsed.path or "") else 0,
560
+ "special_char_count": sum(1 for c in url if c in "!@#$%^&*()+={}[]|\\:;<>?,"),
561
+ }
562
+
563
+
564
+ # ============================================================================
565
+ # TRANSFORMER MODEL LOADER (Phase 3 — real HF transformer models)
566
+ # ============================================================================
567
+ #
568
+ # Loads pretrained BERT-based classifiers from the Hugging Face Hub.
569
+ # Models are loaded lazily on first request to keep cold-start fast.
570
+ # A 7B Security LLM is NOT loaded locally (too big for free tier) —
571
+ # it's accessed via the HF Inference API on demand.
572
+
573
+ class TransformerModelLoader:
574
+ """Loads pretrained Transformer classifiers from the HF Hub on demand."""
575
+
576
+ # Model registry — name → HF repo + task description
577
+ # NOTE: DGA detector uses an inline entropy heuristic, not a transformer
578
+ # (YangYang-Research/dga-detection has a non-standard model config that
579
+ # isn't loadable via transformers.pipeline()).
580
+ REGISTRY = {
581
+ "url_phishing_bert": {
582
+ "repo": "elftsdmr/malware-url-detect",
583
+ "task": "text-classification",
584
+ "labels": ["benign", "malicious"],
585
+ "desc": "BERT-based URL phishing/malware classifier",
586
+ },
587
+ }
588
+ # Model used via HF Inference API. ZySec-AI/SecurityLLM isn't served on the
589
+ # free Inference API tier (404). Mistral-7B-Instruct-v0.3 is widely available
590
+ # and works well for cyber Q&A. Override via SECURITY_LLM_MODEL env var.
591
+ SECURITY_LLM_REPO = os.environ.get("SECURITY_LLM_MODEL", "mistralai/Mistral-7B-Instruct-v0.3")
592
+
593
+ def __init__(self):
594
+ self.pipelines = {} # name → transformers.Pipeline (lazy-loaded)
595
+ self.load_errors = {} # name → last error message
596
+ self.transformers_available = False
597
+ try:
598
+ import transformers # noqa: F401
599
+ import torch # noqa: F401
600
+ self.transformers_available = True
601
+ logger.info("✅ transformers + torch available")
602
+ except ImportError as e:
603
+ logger.warning(f"⚠️ transformers/torch not installed: {e}")
604
+
605
+ def _ensure(self, name: str):
606
+ """Load a pipeline on first use. Returns the pipeline or None on failure."""
607
+ if name in self.pipelines:
608
+ return self.pipelines[name]
609
+ if not self.transformers_available:
610
+ self.load_errors[name] = "transformers/torch not installed"
611
+ return None
612
+ if name not in self.REGISTRY:
613
+ self.load_errors[name] = f"Unknown model: {name}"
614
+ return None
615
+ spec = self.REGISTRY[name]
616
+ try:
617
+ from transformers import pipeline
618
+ logger.info(f"⏳ Loading {name} from {spec['repo']}...")
619
+ pipe = pipeline(spec["task"], model=spec["repo"], device=-1, top_k=None)
620
+ self.pipelines[name] = pipe
621
+ logger.info(f"✅ Loaded {name}")
622
+ return pipe
623
+ except Exception as e:
624
+ err = f"{type(e).__name__}: {str(e)[:200]}"
625
+ self.load_errors[name] = err
626
+ logger.error(f"❌ Failed to load {name}: {err}")
627
+ return None
628
+
629
+ def predict_url_phishing(self, url: str) -> Dict:
630
+ """Classify a URL as benign or malicious using elftsdmr/malware-url-detect."""
631
+ pipe = self._ensure("url_phishing_bert")
632
+ if pipe is None:
633
+ return self._unavailable("url_phishing_bert")
634
+ try:
635
+ # Strip the protocol — model was trained on bare URLs
636
+ text = url.replace("https://", "").replace("http://", "")[:512]
637
+ result = pipe(text)
638
+ scores = result[0] if isinstance(result[0], list) else result
639
+ return self._format_classification(scores, "url_phishing_bert")
640
+ except Exception as e:
641
+ return self._error("url_phishing_bert", e)
642
+
643
+ def predict_dga(self, domain: str) -> Dict:
644
+ """Detect DGA-generated domains using a Shannon-entropy + character-pattern heuristic.
645
+ Real DGA domains have high entropy, low pronounceability, no real word substrings."""
646
+ import math
647
+ d = domain.lower().strip().split('.')[0][:60] # SLD only, ignore TLD
648
+ if not d:
649
+ return {"model": "dga_detector", "error": "empty domain"}
650
+ # Shannon entropy of character distribution
651
+ from collections import Counter
652
+ freq = Counter(d)
653
+ n = len(d)
654
+ entropy = -sum((c / n) * math.log2(c / n) for c in freq.values())
655
+ # Vowel ratio — DGAs typically have very few vowels
656
+ vowels = sum(1 for c in d if c in 'aeiou')
657
+ vowel_ratio = vowels / n if n else 0
658
+ # Digit ratio — DGAs often mix digits in
659
+ digits = sum(1 for c in d if c.isdigit())
660
+ digit_ratio = digits / n if n else 0
661
+ # Length signal — DGAs are usually 10-25 chars
662
+ length_signal = 1.0 if 12 <= n <= 30 else 0.5 if 8 <= n <= 40 else 0.2
663
+ # Consonant runs — DGAs often have 4+ consonants in a row
664
+ max_consonant_run = 0
665
+ run = 0
666
+ for c in d:
667
+ if c.isalpha() and c not in 'aeiou':
668
+ run += 1
669
+ max_consonant_run = max(max_consonant_run, run)
670
+ else:
671
+ run = 0
672
+ # Score combination — empirically tuned thresholds
673
+ score = 0.0
674
+ if entropy > 3.5: score += 0.35
675
+ if vowel_ratio < 0.25: score += 0.20
676
+ if digit_ratio > 0.15: score += 0.15
677
+ if max_consonant_run >= 4: score += 0.20
678
+ score *= length_signal
679
+ score = min(1.0, score)
680
+ is_dga = score >= 0.45
681
+ return {
682
+ "model": "dga_detector",
683
+ "prediction": "dga" if is_dga else "legit",
684
+ "is_threat": is_dga,
685
+ "confidence": round(score * 100, 2) if is_dga else round((1 - score) * 100, 2),
686
+ "threat_score": round(score, 4),
687
+ "features": {
688
+ "entropy": round(entropy, 3),
689
+ "vowel_ratio": round(vowel_ratio, 3),
690
+ "digit_ratio": round(digit_ratio, 3),
691
+ "max_consonant_run": max_consonant_run,
692
+ "length": n,
693
+ },
694
+ "inference_source": "entropy-heuristic",
695
+ }
696
+
697
+ def security_chat(self, query: str, max_tokens: int = 512) -> Dict:
698
+ """Cybersecurity Q&A via ZySec-AI/SecurityLLM hosted on HF Inference API.
699
+ Falls back to Gemini when the LLM is rate-limited or HF token is missing."""
700
+ if not HF_TOKEN:
701
+ return {
702
+ "model": "security-llm",
703
+ "response": None,
704
+ "error": "HF_TOKEN env var required to call ZySec-AI/SecurityLLM via Inference API",
705
+ "fallback_available": gemini_service.ready,
706
+ }
707
+ try:
708
+ import requests
709
+ url = f"https://api-inference.huggingface.co/models/{self.SECURITY_LLM_REPO}"
710
+ headers = {"Authorization": f"Bearer {HF_TOKEN}", "Content-Type": "application/json"}
711
+ payload = {
712
+ "inputs": query[:2000],
713
+ "parameters": {"max_new_tokens": max_tokens, "temperature": 0.3, "return_full_text": False},
714
+ "options": {"wait_for_model": True},
715
+ }
716
+ r = requests.post(url, headers=headers, json=payload, timeout=45)
717
+ if r.status_code == 200:
718
+ data = r.json()
719
+ text = data[0].get("generated_text") if isinstance(data, list) and data else (data.get("generated_text") if isinstance(data, dict) else str(data))
720
+ return {
721
+ "model": "security-llm",
722
+ "source": "huggingface_inference_api",
723
+ "response": text,
724
+ "model_id": self.SECURITY_LLM_REPO,
725
+ }
726
+ return {
727
+ "model": "security-llm",
728
+ "error": f"HF Inference API HTTP {r.status_code}: {r.text[:200]}",
729
+ "fallback_available": gemini_service.ready,
730
+ }
731
+ except Exception as e:
732
+ return {
733
+ "model": "security-llm",
734
+ "error": f"{type(e).__name__}: {str(e)[:200]}",
735
+ "fallback_available": gemini_service.ready,
736
+ }
737
+
738
+ def status(self) -> Dict:
739
+ return {
740
+ "transformers_available": self.transformers_available,
741
+ "loaded": list(self.pipelines.keys()),
742
+ "available": list(self.REGISTRY.keys()) + ["security_llm (via HF Inference API)"],
743
+ "load_errors": self.load_errors,
744
+ }
745
+
746
+ @staticmethod
747
+ def _format_classification(scores, model_name) -> Dict:
748
+ """Normalize HF text-classification output to the cyberforge schema."""
749
+ if not scores:
750
+ return {"model": model_name, "error": "Empty scores"}
751
+ # scores is a list of {label, score} dicts. Find the threat label.
752
+ threat_labels = {"malicious", "phishing", "malware", "dga", "label_1", "1"}
753
+ # Top prediction
754
+ top = max(scores, key=lambda s: s["score"]) if isinstance(scores, list) else scores
755
+ is_threat = str(top["label"]).lower() in threat_labels
756
+ # Threat score: probability of the threat class (not just the top class)
757
+ threat_score = top["score"] if is_threat else 1.0 - top["score"]
758
+ return {
759
+ "model": model_name,
760
+ "prediction": top["label"],
761
+ "is_threat": is_threat,
762
+ "confidence": round(top["score"] * 100, 2),
763
+ "threat_score": round(threat_score, 4),
764
+ "all_scores": scores,
765
+ "inference_source": "huggingface_transformer",
766
+ }
767
+
768
+ @staticmethod
769
+ def _unavailable(model_name) -> Dict:
770
+ return {"model": model_name, "error": "Model unavailable — see /api/v2/status"}
771
+
772
+ @staticmethod
773
+ def _error(model_name, e: Exception) -> Dict:
774
+ return {"model": model_name, "error": f"{type(e).__name__}: {str(e)[:200]}"}
775
+
776
+
777
+ transformer_loader = TransformerModelLoader()
778
+
779
+
780
+ # ============================================================================
781
+ # NOTEBOOK EXECUTION (existing Gradio functionality)
782
+ # ============================================================================
783
+
784
+ def get_available_notebooks() -> List[str]:
785
+ if not NOTEBOOKS_DIR.exists():
786
+ return []
787
+ return sorted([f.name for f in NOTEBOOKS_DIR.glob("*.ipynb")])
788
+
789
+
790
+ def read_notebook_content(notebook_name: str) -> str:
791
+ """Read and display notebook content as markdown"""
792
+ notebook_path = NOTEBOOKS_DIR / notebook_name
793
+ if not notebook_path.exists():
794
+ return f"Notebook not found: {notebook_name}"
795
+ try:
796
+ with open(notebook_path, "r") as f:
797
+ nb = json.load(f)
798
+ output = f"# {notebook_name}\n\n"
799
+ for i, cell in enumerate(nb.get("cells", []), 1):
800
+ cell_type = cell.get("cell_type", "code")
801
+ source = "".join(cell.get("source", []))
802
+ if cell_type == "markdown":
803
+ output += f"{source}\n\n"
804
+ else:
805
+ output += f"### Cell {i} (Python)\n```python\n{source}\n```\n\n"
806
+ return output
807
+ except Exception as e:
808
+ return f"Error reading notebook: {str(e)}"
809
+
810
+
811
+ def execute_notebook(notebook_name: str, progress=gr.Progress()) -> Tuple[str, str]:
812
+ """Execute a notebook and return output"""
813
+ notebook_path = NOTEBOOKS_DIR / notebook_name
814
+ output_path = NOTEBOOKS_DIR / f"output_{notebook_name}"
815
+ if not notebook_path.exists():
816
+ available = list(NOTEBOOKS_DIR.glob("*.ipynb")) if NOTEBOOKS_DIR.exists() else []
817
+ return f"Error: Notebook not found: {notebook_path}\nAvailable: {available}", ""
818
+ progress(0.1, desc="Starting notebook execution...")
819
+ try:
820
+ cmd = [
821
+ sys.executable, "-m", "nbconvert",
822
+ "--to", "notebook", "--execute",
823
+ "--output", str(output_path.absolute()),
824
+ "--ExecutePreprocessor.timeout=600",
825
+ "--ExecutePreprocessor.kernel_name=python3",
826
+ str(notebook_path.absolute()),
827
+ ]
828
+ progress(0.3, desc="Executing cells...")
829
+ result = subprocess.run(cmd, capture_output=True, text=True, cwd=str(NOTEBOOKS_DIR), timeout=900)
830
+ progress(0.8, desc="Processing output...")
831
+ if result.returncode == 0:
832
+ if output_path.exists():
833
+ with open(output_path, "r") as f:
834
+ executed_nb = json.load(f)
835
+ outputs = []
836
+ for i, cell in enumerate(executed_nb.get("cells", []), 1):
837
+ if cell.get("cell_type") == "code":
838
+ for out in cell.get("outputs", []):
839
+ if "text" in out:
840
+ outputs.append(f"Cell {i}:\n{''.join(out['text'])}")
841
+ elif "data" in out and "text/plain" in out["data"]:
842
+ outputs.append(f"Cell {i}:\n{''.join(out['data']['text/plain'])}")
843
+ progress(1.0, desc="Complete!")
844
+ return "Notebook executed successfully!", "\n\n".join(outputs)
845
+ else:
846
+ return "Notebook executed but output file not found", result.stdout
847
+ else:
848
+ return f"Execution failed:\n{result.stderr}", result.stdout
849
+ except subprocess.TimeoutExpired:
850
+ return "Error: Notebook execution timed out (15 min limit)", ""
851
+ except Exception as e:
852
+ return f"Error executing notebook: {str(e)}", ""
853
+
854
+
855
+ def run_notebook_cell(notebook_name: str, cell_number: int) -> str:
856
+ """Execute a single cell from a notebook"""
857
+ notebook_path = NOTEBOOKS_DIR / notebook_name
858
+ if not notebook_path.exists():
859
+ return f"Error: Notebook not found at {notebook_path}"
860
+ try:
861
+ original_cwd = os.getcwd()
862
+ os.chdir(NOTEBOOKS_DIR)
863
+ with open(notebook_path, "r") as f:
864
+ nb = json.load(f)
865
+ cells = [c for c in nb.get("cells", []) if c.get("cell_type") == "code"]
866
+ if cell_number < 1 or cell_number > len(cells):
867
+ os.chdir(original_cwd)
868
+ return f"Error: Cell {cell_number} not found. Available: 1-{len(cells)}"
869
+ cell = cells[int(cell_number) - 1]
870
+ source = "".join(cell.get("source", []))
871
+ import io
872
+ from contextlib import redirect_stdout, redirect_stderr
873
+
874
+ namespace = {"__name__": "__main__", "__file__": str(notebook_path)}
875
+ stdout_capture = io.StringIO()
876
+ stderr_capture = io.StringIO()
877
+ with redirect_stdout(stdout_capture), redirect_stderr(stderr_capture):
878
+ try:
879
+ exec(source, namespace)
880
+ except Exception as e:
881
+ os.chdir(original_cwd)
882
+ return f"Error: {str(e)}"
883
+ os.chdir(original_cwd)
884
+ output = stdout_capture.getvalue()
885
+ errors = stderr_capture.getvalue()
886
+ result_text = f"### Cell {int(cell_number)} Output:\n"
887
+ if output:
888
+ result_text += f"```\n{output}\n```\n"
889
+ if errors:
890
+ result_text += f"\n**Warnings/Errors:**\n```\n{errors}\n```"
891
+ if not output and not errors:
892
+ result_text += "*(No output)*"
893
+ return result_text
894
+ except Exception as e:
895
+ try:
896
+ os.chdir(original_cwd)
897
+ except Exception:
898
+ pass
899
+ return f"Error: {str(e)}"
900
+
901
+
902
+ # ============================================================================
903
+ # MODEL TRAINING (existing Gradio functionality)
904
+ # ============================================================================
905
+
906
+ class SecurityModelTrainer:
907
+ def __init__(self):
908
+ self.scaler = StandardScaler()
909
+ self.label_encoder = LabelEncoder()
910
+
911
+ def prepare_data(self, df: pd.DataFrame, target_col: str = "label") -> Tuple:
912
+ if target_col not in df.columns:
913
+ raise ValueError(f"Target column '{target_col}' not found")
914
+ X = df.drop(columns=[target_col])
915
+ y = df[target_col]
916
+ X = X.select_dtypes(include=[np.number]).fillna(0)
917
+ if y.dtype == "object":
918
+ y = self.label_encoder.fit_transform(y)
919
+ X_scaled = self.scaler.fit_transform(X)
920
+ return train_test_split(X_scaled, y, test_size=0.2, random_state=42)
921
+
922
+ def train_model(self, model_type: str, X_train, y_train):
923
+ if model_type not in MODEL_TYPES:
924
+ raise ValueError(f"Unknown model type: {model_type}")
925
+ model_class = MODEL_TYPES[model_type]
926
+ if model_type == "Isolation Forest (Anomaly)":
927
+ model = model_class(contamination=0.1, random_state=42)
928
+ else:
929
+ model = model_class(random_state=42)
930
+ model.fit(X_train, y_train)
931
+ return model
932
+
933
+ def evaluate_model(self, model, X_test, y_test) -> Dict:
934
+ y_pred = model.predict(X_test)
935
+ return {
936
+ "accuracy": accuracy_score(y_test, y_pred),
937
+ "f1_score": f1_score(y_test, y_pred, average="weighted", zero_division=0),
938
+ }
939
+
940
+
941
+ trainer = SecurityModelTrainer()
942
+
943
+
944
+ def train_model_from_data(data_file, model_type: str, task: str, progress=gr.Progress()):
945
+ """Train model from uploaded data"""
946
+ if data_file is None:
947
+ return "Please upload a CSV file", None, None
948
+ progress(0.1, desc="Loading data...")
949
+ try:
950
+ df = pd.read_csv(data_file.name)
951
+ progress(0.3, desc="Preparing data...")
952
+ X_train, X_test, y_train, y_test = trainer.prepare_data(df)
953
+ progress(0.5, desc=f"Training {model_type}...")
954
+ model = trainer.train_model(model_type, X_train, y_train)
955
+ progress(0.8, desc="Evaluating model...")
956
+ metrics = trainer.evaluate_model(model, X_test, y_test)
957
+ model_name = f"{task.lower().replace(' ', '_')}_{model_type.lower().replace(' ', '_')}"
958
+ model_path = MODELS_DIR / f"{model_name}.pkl"
959
+ joblib.dump(model, model_path)
960
+ progress(1.0, desc="Complete!")
961
+ result = f"""
962
+ ## Training Complete!
963
+
964
+ **Task:** {task}
965
+ **Model:** {model_type}
966
+ **Samples:** {len(df)}
967
+
968
+ ### Metrics
969
+ - Accuracy: {metrics['accuracy']:.4f}
970
+ - F1 Score: {metrics['f1_score']:.4f}
971
+
972
+ **Model saved to:** {model_path}
973
+ """
974
+ return result, str(model_path), json.dumps(metrics, indent=2)
975
+ except Exception as e:
976
+ return f"Error: {str(e)}", None, None
977
+
978
+
979
+ def run_inference(model_file, features_text: str):
980
+ """Run inference with a trained model"""
981
+ if model_file is None:
982
+ return "Please upload a model file"
983
+ try:
984
+ model = joblib.load(model_file.name)
985
+ features = json.loads(features_text)
986
+ X = np.array([list(features.values())])
987
+ prediction = model.predict(X)[0]
988
+ result = {"prediction": int(prediction), "features_used": len(features)}
989
+ if hasattr(model, "predict_proba"):
990
+ proba = model.predict_proba(X)[0]
991
+ result["confidence"] = float(max(proba))
992
+ result["probabilities"] = {str(i): float(p) for i, p in enumerate(proba)}
993
+ return json.dumps(result, indent=2)
994
+ except Exception as e:
995
+ return f"Error: {str(e)}"
996
+
997
+
998
+ def list_trained_models():
999
+ models = list(MODELS_DIR.glob("*.pkl"))
1000
+ if not models:
1001
+ return "No trained models found"
1002
+ output = "## Trained Models\n\n"
1003
+ for model_path in models:
1004
+ size_kb = model_path.stat().st_size / 1024
1005
+ output += f"- **{model_path.name}** ({size_kb:.1f} KB)\n"
1006
+ return output
1007
+
1008
+
1009
+ # ============================================================================
1010
+ # FASTAPI APP (REST endpoints for Heroku backend mlService.js)
1011
+ # ============================================================================
1012
+
1013
+ api = FastAPI(title="CyberForge AI API", version="1.0.0")
1014
+
1015
+ api.add_middleware(
1016
+ CORSMiddleware,
1017
+ allow_origins=["*"],
1018
+ allow_credentials=True,
1019
+ allow_methods=["*"],
1020
+ allow_headers=["*"],
1021
+ )
1022
+
1023
+
1024
+ @api.get("/health")
1025
+ async def api_health():
1026
+ return {
1027
+ "status": "healthy",
1028
+ "timestamp": datetime.utcnow().isoformat(),
1029
+ "services": {
1030
+ "gemini": gemini_service.ready,
1031
+ "ml_models": ml_loader.ready,
1032
+ "models_loaded": list(ml_loader.models.keys()),
1033
+ "transformers": transformer_loader.transformers_available,
1034
+ "transformer_models_loaded": list(transformer_loader.pipelines.keys()),
1035
+ "gradio_ui": True,
1036
+ },
1037
+ "version": "2.0.0",
1038
+ "new_endpoints": [
1039
+ "POST /api/v2/batch-analyze",
1040
+ "GET /api/v2/explain/{model_name}",
1041
+ "POST /api/v2/ioc-scan",
1042
+ "POST /api/v2/url-enrich",
1043
+ "POST /api/v2/chat (context-grounded security chatbot, Wave 2)",
1044
+ "POST /api/v2/security-chat",
1045
+ ],
1046
+ }
1047
+
1048
+
1049
+ @api.post("/analyze")
1050
+ async def api_analyze(request: Request):
1051
+ """Main analysis endpoint – called by backend mlService.chatWithAI()"""
1052
+ body = await request.json()
1053
+ query = body.get("query", "")
1054
+ context = body.get("context", {})
1055
+ history = body.get("conversation_history", [])
1056
+ result = gemini_service.analyze(query, context, history)
1057
+ return result
1058
+
1059
+
1060
+ @api.post("/analyze-url")
1061
+ async def api_analyze_url(request: Request):
1062
+ """URL analysis – called by backend mlService.analyzeWebsite()
1063
+ Returns per-model predictions + unified aggregate with risk_score (0-100).
1064
+ All models now consistently include threat_score (probability of class-1).
1065
+ """
1066
+ body = await request.json()
1067
+ url = body.get("url", "")
1068
+ if not url:
1069
+ return JSONResponse(status_code=400, content={"detail": "URL required"})
1070
+ features = extract_url_features(url)
1071
+ predictions = {}
1072
+ for model_name in ml_loader.MODEL_NAMES:
1073
+ predictions[model_name] = ml_loader.predict(model_name, features)
1074
+
1075
+ # threat_score is now always present on every prediction (ml_model + heuristic)
1076
+ scores = [p.get("threat_score", 0.2) for p in predictions.values()]
1077
+ avg_score = sum(scores) / len(scores) if scores else 0
1078
+ max_score = max(scores) if scores else 0
1079
+
1080
+ # Heuristic risk factors derived from URL features
1081
+ risk_factors = []
1082
+ if not features.get("is_https"):
1083
+ risk_factors.append("no_https")
1084
+ if features.get("has_ip_address"):
1085
+ risk_factors.append("ip_in_url")
1086
+ if features.get("has_suspicious_tld"):
1087
+ risk_factors.append("suspicious_tld")
1088
+ if features.get("url_length", 0) > 100:
1089
+ risk_factors.append("long_url")
1090
+ if features.get("has_at_symbol"):
1091
+ risk_factors.append("at_symbol_in_url")
1092
+ if features.get("subdomain_count", 0) > 2:
1093
+ risk_factors.append("excessive_subdomains")
1094
+ if features.get("special_char_count", 0) > 10:
1095
+ risk_factors.append("high_special_chars")
1096
+ if features.get("has_double_slash"):
1097
+ risk_factors.append("double_slash_in_path")
1098
+
1099
+ # Overall risk level using the max score (conservative)
1100
+ if max_score > 0.8:
1101
+ overall_risk = "critical"
1102
+ elif max_score > 0.6:
1103
+ overall_risk = "high"
1104
+ elif max_score > 0.4:
1105
+ overall_risk = "medium"
1106
+ else:
1107
+ overall_risk = "low"
1108
+
1109
+ # risk_score 0-100 for the Heroku backend (used in riskScore field)
1110
+ risk_score_100 = round(max_score * 100, 1)
1111
+
1112
+ return {
1113
+ "url": url,
1114
+ "aggregate": {
1115
+ "average_threat_score": round(avg_score, 3),
1116
+ "max_threat_score": round(max_score, 3),
1117
+ "risk_score": risk_score_100,
1118
+ "overall_risk_level": overall_risk,
1119
+ "risk_factors": risk_factors,
1120
+ "models_flagged": sum(1 for p in predictions.values() if p.get("prediction", 0) == 1),
1121
+ "models_total": len(predictions),
1122
+ },
1123
+ "model_predictions": predictions,
1124
+ "features_analyzed": features,
1125
+ "timestamp": datetime.utcnow().isoformat(),
1126
+ }
1127
+
1128
+
1129
+ @api.post("/scan-threats")
1130
+ async def api_scan_threats(request: Request):
1131
+ """Threat scanning – called by backend mlService.scanForThreats()"""
1132
+ body = await request.json()
1133
+ query = json.dumps(body.get("data", body), indent=1)[:3000]
1134
+ result = gemini_service.analyze(f"Scan these indicators for threats:\n{query}")
1135
+ return result
1136
+
1137
+
1138
+ @api.post("/api/insights/generate")
1139
+ async def api_generate_insights(request: Request):
1140
+ """AI insights – called by backend mlService.getAIInsights()"""
1141
+ body = await request.json()
1142
+ query = body.get("query", "")
1143
+ context = body.get("context", {})
1144
+ result = gemini_service.analyze(query, context)
1145
+ return {
1146
+ "insights": result.get("response", ""),
1147
+ "confidence": result.get("confidence", 0),
1148
+ "timestamp": datetime.utcnow().isoformat(),
1149
+ }
1150
+
1151
+
1152
+ @api.post("/api/models/predict")
1153
+ async def api_model_predict(request: Request):
1154
+ """Model prediction – called by backend mlService.getModelPrediction()"""
1155
+ body = await request.json()
1156
+ model_type = body.get("model_type", "phishing_detection")
1157
+ input_data = body.get("input_data", {})
1158
+ result = ml_loader.predict(model_type, input_data)
1159
+ return result
1160
+
1161
+
1162
+ @api.get("/api/models/list")
1163
+ @api.get("/models")
1164
+ async def api_list_models():
1165
+ result = []
1166
+ for name in ml_loader.MODEL_NAMES:
1167
+ result.append({
1168
+ "name": name,
1169
+ "loaded": name in ml_loader.models,
1170
+ "source": "ml_model" if name in ml_loader.models else "heuristic",
1171
+ })
1172
+ return {"models": result, "total": len(ml_loader.MODEL_NAMES), "loaded": len(ml_loader.models)}
1173
+
1174
+
1175
+ # ============================================================================
1176
+ # PHASE-3 ENDPOINTS — Real HF Transformer models
1177
+ # ============================================================================
1178
+
1179
+ @api.post("/api/v2/url-classify")
1180
+ async def api_v2_url_classify(request: Request):
1181
+ """URL phishing/malware classification using elftsdmr/malware-url-detect (BERT).
1182
+ Body: { "url": "https://..." }"""
1183
+ body = await request.json()
1184
+ url = body.get("url", "").strip()
1185
+ if not url:
1186
+ return JSONResponse(status_code=400, content={"detail": "url required"})
1187
+ return transformer_loader.predict_url_phishing(url)
1188
+
1189
+
1190
+ @api.post("/api/v2/dga-detect")
1191
+ async def api_v2_dga_detect(request: Request):
1192
+ """DGA-generated domain detection using YangYang-Research/dga-detection.
1193
+ Body: { "domain": "abc123xyz.com" }"""
1194
+ body = await request.json()
1195
+ domain = body.get("domain", "").strip()
1196
+ if not domain:
1197
+ return JSONResponse(status_code=400, content={"detail": "domain required"})
1198
+ return transformer_loader.predict_dga(domain)
1199
+
1200
+
1201
+ @api.post("/api/v2/security-chat")
1202
+ async def api_v2_security_chat(request: Request):
1203
+ """Cybersecurity Q&A via ZySec-AI/SecurityLLM (HF Inference API).
1204
+ Body: { "query": "...", "max_tokens": 512 }"""
1205
+ body = await request.json()
1206
+ query = body.get("query", "").strip()
1207
+ if not query:
1208
+ return JSONResponse(status_code=400, content={"detail": "query required"})
1209
+ max_tokens = int(body.get("max_tokens", 512))
1210
+ result = transformer_loader.security_chat(query, max_tokens=max_tokens)
1211
+ # Auto-fallback to Gemini when LLM unavailable
1212
+ if result.get("error") and gemini_service.ready:
1213
+ gemini_result = gemini_service.analyze(query)
1214
+ gemini_result["source"] = "gemini-fallback"
1215
+ gemini_result["llm_error"] = result["error"]
1216
+ return gemini_result
1217
+ return result
1218
+
1219
+
1220
+ @api.get("/api/v2/status")
1221
+ async def api_v2_status():
1222
+ """Status of phase-3 transformer models — what's loaded, what failed, what's available."""
1223
+ return transformer_loader.status()
1224
+
1225
+
1226
+ @api.post("/api/v2/batch-analyze")
1227
+ async def api_v2_batch_analyze(request: Request):
1228
+ """Batch URL analysis for the distributed agent system.
1229
+ Accepts up to 20 URLs in one call — avoids the per-URL HTTP overhead
1230
+ when multiple agents run concurrent scans.
1231
+
1232
+ Body:
1233
+ { "urls": ["https://...", "https://...", ...] }
1234
+
1235
+ Response:
1236
+ { "results": [ { ...same shape as /analyze-url... }, ... ], "total": N, "elapsed_ms": N }
1237
+ """
1238
+ import time
1239
+ body = await request.json()
1240
+ urls = body.get("urls", [])
1241
+ if not urls:
1242
+ return JSONResponse(status_code=400, content={"detail": "urls array required"})
1243
+ if len(urls) > 20:
1244
+ return JSONResponse(status_code=400, content={"detail": "Maximum 20 URLs per batch"})
1245
+
1246
+ t0 = time.time()
1247
+ results = []
1248
+ for url in urls:
1249
+ url = str(url).strip()
1250
+ if not url:
1251
+ results.append({"url": url, "error": "empty url"})
1252
+ continue
1253
+ features = extract_url_features(url)
1254
+ predictions = {}
1255
+ for model_name in ml_loader.MODEL_NAMES:
1256
+ predictions[model_name] = ml_loader.predict(model_name, features)
1257
+ scores = [p.get("threat_score", 0.2) for p in predictions.values()]
1258
+ avg_score = sum(scores) / len(scores) if scores else 0
1259
+ max_score = max(scores) if scores else 0
1260
+ risk_factors = []
1261
+ if not features.get("is_https"):
1262
+ risk_factors.append("no_https")
1263
+ if features.get("has_ip_address"):
1264
+ risk_factors.append("ip_in_url")
1265
+ if features.get("has_suspicious_tld"):
1266
+ risk_factors.append("suspicious_tld")
1267
+ if features.get("url_length", 0) > 100:
1268
+ risk_factors.append("long_url")
1269
+ if features.get("has_at_symbol"):
1270
+ risk_factors.append("at_symbol_in_url")
1271
+ if features.get("subdomain_count", 0) > 2:
1272
+ risk_factors.append("excessive_subdomains")
1273
+ if max_score > 0.8:
1274
+ overall_risk = "critical"
1275
+ elif max_score > 0.6:
1276
+ overall_risk = "high"
1277
+ elif max_score > 0.4:
1278
+ overall_risk = "medium"
1279
+ else:
1280
+ overall_risk = "low"
1281
+ results.append({
1282
+ "url": url,
1283
+ "aggregate": {
1284
+ "average_threat_score": round(avg_score, 3),
1285
+ "max_threat_score": round(max_score, 3),
1286
+ "risk_score": round(max_score * 100, 1),
1287
+ "overall_risk_level": overall_risk,
1288
+ "risk_factors": risk_factors,
1289
+ "models_flagged": sum(1 for p in predictions.values() if p.get("prediction", 0) == 1),
1290
+ },
1291
+ "model_predictions": predictions,
1292
+ "features_analyzed": features,
1293
+ })
1294
+
1295
+ elapsed_ms = round((time.time() - t0) * 1000, 1)
1296
+ return {
1297
+ "results": results,
1298
+ "total": len(results),
1299
+ "elapsed_ms": elapsed_ms,
1300
+ "timestamp": datetime.utcnow().isoformat(),
1301
+ }
1302
+
1303
+
1304
+ @api.get("/api/v2/explain/{model_name}")
1305
+ async def api_v2_explain(model_name: str):
1306
+ """Feature importance / explainability for a loaded ML model.
1307
+ Supports RandomForest, GradientBoosting (gini importance) and LogisticRegression (coefficients).
1308
+
1309
+ Path param: model_name — one of phishing_detection | malware_detection |
1310
+ anomaly_detection | web_attack_detection
1311
+ """
1312
+ return ml_loader.feature_importance(model_name)
1313
+
1314
+
1315
+ @api.post("/api/v2/ioc-scan")
1316
+ async def api_v2_ioc_scan(request: Request):
1317
+ """Multi-field Indicator of Compromise scanner.
1318
+ Runs all available ML models + DGA detector + BERT classifier against
1319
+ multiple IOC types in a single call.
1320
+
1321
+ Body:
1322
+ {
1323
+ "url": "https://...", # optional
1324
+ "domain": "example.com", # optional
1325
+ "ip": "1.2.3.4", # optional
1326
+ "hash": "abc123...", # optional (future use)
1327
+ "context": { ... } # optional — passed to Gemini if available
1328
+ }
1329
+
1330
+ Response: per-indicator results with unified risk summary.
1331
+ """
1332
+ body = await request.json()
1333
+ url = body.get("url", "").strip()
1334
+ domain = body.get("domain", "").strip()
1335
+ context = body.get("context", {})
1336
+
1337
+ ioc_results: Dict[str, Any] = {}
1338
+ all_scores: List[float] = []
1339
+
1340
+ # --- URL analysis ---
1341
+ if url:
1342
+ features = extract_url_features(url)
1343
+ url_preds = {name: ml_loader.predict(name, features) for name in ml_loader.MODEL_NAMES}
1344
+ url_scores = [p.get("threat_score", 0.2) for p in url_preds.values()]
1345
+ url_max = max(url_scores) if url_scores else 0
1346
+ all_scores.append(url_max)
1347
+
1348
+ # BERT-based URL classifier (lazy-loaded)
1349
+ bert_result = transformer_loader.predict_url_phishing(url)
1350
+ ioc_results["url"] = {
1351
+ "value": url,
1352
+ "ml_predictions": url_preds,
1353
+ "bert_classification": bert_result,
1354
+ "max_threat_score": round(url_max, 3),
1355
+ }
1356
+
1357
+ # --- Domain analysis ---
1358
+ effective_domain = domain
1359
+ if not effective_domain and url:
1360
+ from urllib.parse import urlparse as _urlparse
1361
+ effective_domain = _urlparse(url).hostname or ""
1362
+
1363
+ if effective_domain:
1364
+ dga_result = transformer_loader.predict_dga(effective_domain)
1365
+ dga_score = dga_result.get("threat_score", 0.0)
1366
+ all_scores.append(dga_score)
1367
+ ioc_results["domain"] = {
1368
+ "value": effective_domain,
1369
+ "dga_detection": dga_result,
1370
+ }
1371
+
1372
+ # --- Overall risk summary ---
1373
+ overall_score = max(all_scores) if all_scores else 0.0
1374
+ if overall_score > 0.8:
1375
+ risk_level = "critical"
1376
+ elif overall_score > 0.6:
1377
+ risk_level = "high"
1378
+ elif overall_score > 0.4:
1379
+ risk_level = "medium"
1380
+ elif overall_score > 0.0:
1381
+ risk_level = "low"
1382
+ else:
1383
+ risk_level = "unknown"
1384
+
1385
+ # Optional Gemini enrichment when available
1386
+ ai_analysis = None
1387
+ if gemini_service.ready and (url or domain):
1388
+ target = url or domain
1389
+ ai_analysis = gemini_service.analyze(
1390
+ f"Scan these IOC indicators for threats: url={target}, domain={effective_domain}",
1391
+ context=context,
1392
+ )
1393
+
1394
+ return {
1395
+ "ioc_results": ioc_results,
1396
+ "summary": {
1397
+ "overall_risk_level": risk_level,
1398
+ "overall_threat_score": round(overall_score, 3),
1399
+ "risk_score": round(overall_score * 100, 1),
1400
+ "indicators_analyzed": len(ioc_results),
1401
+ },
1402
+ "ai_analysis": ai_analysis,
1403
+ "timestamp": datetime.utcnow().isoformat(),
1404
+ }
1405
+
1406
+
1407
+ @api.post("/api/v2/url-enrich")
1408
+ async def api_v2_url_enrich(request: Request):
1409
+ """Enriched URL analysis: combines heuristic + ML models + BERT transformer
1410
+ into a single scored response with explanations.
1411
+ This is the richer alternative to /analyze-url for the AI Deep Scan mode.
1412
+
1413
+ Body: { "url": "https://..." }
1414
+ """
1415
+ body = await request.json()
1416
+ url = body.get("url", "").strip()
1417
+ if not url:
1418
+ return JSONResponse(status_code=400, content={"detail": "url required"})
1419
+
1420
+ features = extract_url_features(url)
1421
+
1422
+ # Core ML predictions
1423
+ ml_predictions = {name: ml_loader.predict(name, features) for name in ml_loader.MODEL_NAMES}
1424
+ ml_scores = [p.get("threat_score", 0.2) for p in ml_predictions.values()]
1425
+ ml_max = max(ml_scores) if ml_scores else 0
1426
+
1427
+ # BERT transformer
1428
+ bert_result = transformer_loader.predict_url_phishing(url)
1429
+ bert_score = bert_result.get("threat_score", 0.0) if not bert_result.get("error") else None
1430
+
1431
+ # DGA check on hostname
1432
+ from urllib.parse import urlparse as _urlparse
1433
+ hostname = _urlparse(url).hostname or ""
1434
+ dga_result = transformer_loader.predict_dga(hostname) if hostname else None
1435
+ dga_score = dga_result.get("threat_score", 0.0) if dga_result and not dga_result.get("error") else 0.0
1436
+
1437
+ # Feature importances for top model
1438
+ top_model = ml_loader.MODEL_NAMES[0]
1439
+ top_score = ml_scores[0]
1440
+ for i, score in enumerate(ml_scores):
1441
+ if score > top_score:
1442
+ top_score = score
1443
+ top_model = ml_loader.MODEL_NAMES[i]
1444
+ explainability = ml_loader.feature_importance(top_model)
1445
+
1446
+ # Fuse scores: weighted average of ML (60%), BERT (30% if available), DGA (10%)
1447
+ if bert_score is not None:
1448
+ fused_score = 0.60 * ml_max + 0.30 * bert_score + 0.10 * dga_score
1449
+ else:
1450
+ fused_score = 0.80 * ml_max + 0.20 * dga_score
1451
+
1452
+ if fused_score > 0.8:
1453
+ risk_level = "critical"
1454
+ elif fused_score > 0.6:
1455
+ risk_level = "high"
1456
+ elif fused_score > 0.4:
1457
+ risk_level = "medium"
1458
+ else:
1459
+ risk_level = "low"
1460
+
1461
+ risk_factors = []
1462
+ if not features.get("is_https"):
1463
+ risk_factors.append("no_https")
1464
+ if features.get("has_ip_address"):
1465
+ risk_factors.append("ip_in_url")
1466
+ if features.get("has_suspicious_tld"):
1467
+ risk_factors.append("suspicious_tld")
1468
+ if features.get("url_length", 0) > 100:
1469
+ risk_factors.append("long_url")
1470
+ if features.get("has_at_symbol"):
1471
+ risk_factors.append("at_symbol_in_url")
1472
+ if features.get("subdomain_count", 0) > 2:
1473
+ risk_factors.append("excessive_subdomains")
1474
+ if dga_result and dga_result.get("is_threat"):
1475
+ risk_factors.append("dga_domain")
1476
+ if bert_result and bert_result.get("is_threat"):
1477
+ risk_factors.append("bert_flagged_malicious")
1478
+
1479
+ return {
1480
+ "url": url,
1481
+ "risk_level": risk_level,
1482
+ "risk_score": round(fused_score * 100, 1),
1483
+ "fused_threat_score": round(fused_score, 4),
1484
+ "components": {
1485
+ "ml_max_threat_score": round(ml_max, 4),
1486
+ "bert_threat_score": round(bert_score, 4) if bert_score is not None else None,
1487
+ "dga_threat_score": round(dga_score, 4),
1488
+ },
1489
+ "ml_predictions": ml_predictions,
1490
+ "bert_classification": bert_result,
1491
+ "dga_detection": dga_result,
1492
+ "explainability": explainability,
1493
+ "risk_factors": risk_factors,
1494
+ "features_analyzed": features,
1495
+ "timestamp": datetime.utcnow().isoformat(),
1496
+ }
1497
+
1498
+
1499
+ @api.post("/api/v2/chat")
1500
+ async def api_v2_chat(request: Request):
1501
+ """Security Chatbot — Wave 2 implementation.
1502
+
1503
+ Context-grounded security assistant that:
1504
+ 1. Accepts a live context blob (telemetry, scan history, IOCs) and
1505
+ grounds every answer in it (RAG-lite over the current request — no
1506
+ persistent vector store, which requires paid GPU on free tier).
1507
+ 2. Translates raw ML signals / IOCs into plain human language when the
1508
+ user includes raw data and asks for an explanation.
1509
+ 3. Falls back to the Security LLM (Mistral-7B via HF Inference API)
1510
+ or the ML-powered heuristic analyzer when Gemini is offline.
1511
+
1512
+ NOTE ON REAL-TIME "LEARNING": HuggingFace Spaces cpu-basic does NOT
1513
+ support fine-tuning or persistent model updates at runtime. What this
1514
+ endpoint does instead is context injection — every turn receives the
1515
+ latest system telemetry + scan history in the prompt, so the chatbot's
1516
+ answers are grounded in live data without weight updates. This is the
1517
+ correct RAG-lite pattern for free-tier inference.
1518
+
1519
+ Request body (stable contract):
1520
+ {
1521
+ "message": "What is the risk of this domain: evil.xyz?",
1522
+ "session_id": "optional-uuid-for-conversation-continuity",
1523
+ "conversation_history": [ {"role": "user", "content": "..."}, ... ],
1524
+ "context": {
1525
+ "telemetry": { "cpu": 42, "ram": 67, "net_in_kbps": 120, ... },
1526
+ "recent_scans": [ { "url": "...", "risk_score": 78, "category": "phishing" }, ... ],
1527
+ "active_threats": [ { "type": "phishing", "severity": "high", "source": "..." } ],
1528
+ "behavioral_alerts": [ { "pattern": "...", "score": 0.9 } ],
1529
+ "translate": true # if true → force plain-language IOC translation mode
1530
+ }
1531
+ }
1532
+
1533
+ Response contract (preserved from Wave 1):
1534
+ {
1535
+ "response": "...", # natural language answer grounded in context
1536
+ "confidence": 0.0-1.0,
1537
+ "risk_level": "low|medium|high|critical",
1538
+ "risk_score": 0-100,
1539
+ "model_used": "...",
1540
+ "session_id": "...",
1541
+ "sources": [ { "type": "...", "label": "...", "value": "..." } ],
1542
+ "timestamp": "..."
1543
+ }
1544
+ """
1545
+ body = await request.json()
1546
+ message = body.get("message", "").strip()
1547
+ session_id = body.get("session_id", "")
1548
+ history = body.get("conversation_history", [])
1549
+ context = body.get("context", {})
1550
+
1551
+ if not message:
1552
+ return JSONResponse(status_code=400, content={"detail": "message required"})
1553
+
1554
+ # ── Build grounding context string and source citations ──────────────
1555
+ grounding_lines: List[str] = []
1556
+ sources: List[Dict] = []
1557
+
1558
+ # System telemetry
1559
+ telemetry = context.get("telemetry", {})
1560
+ if telemetry:
1561
+ cpu = telemetry.get("cpu", telemetry.get("cpu_percent"))
1562
+ ram = telemetry.get("ram", telemetry.get("ram_percent"))
1563
+ net_in = telemetry.get("net_in_kbps", telemetry.get("net_in"))
1564
+ telem_parts = []
1565
+ if cpu is not None:
1566
+ telem_parts.append(f"CPU {cpu}%")
1567
+ if ram is not None:
1568
+ telem_parts.append(f"RAM {ram}%")
1569
+ if net_in is not None:
1570
+ telem_parts.append(f"Net↓ {net_in} kbps")
1571
+ if telem_parts:
1572
+ grounding_lines.append(f"SYSTEM TELEMETRY (live, last 5s): {', '.join(telem_parts)}")
1573
+ sources.append({"type": "telemetry", "label": "System Telemetry", "value": ", ".join(telem_parts)})
1574
+
1575
+ # Recent scan history
1576
+ recent_scans = context.get("recent_scans", [])
1577
+ if recent_scans:
1578
+ scan_summary = "; ".join(
1579
+ f"{s.get('url', 'unknown')} → {s.get('category', '?')} (score {s.get('risk_score', '?')}/100)"
1580
+ for s in recent_scans[:5]
1581
+ )
1582
+ grounding_lines.append(f"RECENT URL SCANS (last 5): {scan_summary}")
1583
+ sources.append({"type": "scan_history", "label": "Recent Scans", "value": f"{len(recent_scans)} scans"})
1584
+
1585
+ # Active threats
1586
+ active_threats = context.get("active_threats", [])
1587
+ if active_threats:
1588
+ threat_summary = "; ".join(
1589
+ f"{t.get('type', '?')} [{t.get('severity', '?')}] from {t.get('source', '?')}"
1590
+ for t in active_threats[:5]
1591
+ )
1592
+ grounding_lines.append(f"ACTIVE THREATS: {threat_summary}")
1593
+ sources.append({"type": "threats", "label": "Active Threats", "value": f"{len(active_threats)} active"})
1594
+
1595
+ # Behavioral alerts
1596
+ behavioral_alerts = context.get("behavioral_alerts", [])
1597
+ if behavioral_alerts:
1598
+ alert_summary = "; ".join(
1599
+ f"'{a.get('pattern', a.get('type', '?'))}' (score {a.get('score', '?')})"
1600
+ for a in behavioral_alerts[:3]
1601
+ )
1602
+ grounding_lines.append(f"BEHAVIORAL ALERTS: {alert_summary}")
1603
+ sources.append({"type": "behavioral", "label": "Behavioral Alerts", "value": f"{len(behavioral_alerts)} alerts"})
1604
+
1605
+ # Machine→Human translation mode
1606
+ translate_mode = context.get("translate", False)
1607
+ raw_data = context.get("raw_data", "")
1608
+ if translate_mode and raw_data:
1609
+ grounding_lines.append(
1610
+ f"RAW DATA TO TRANSLATE INTO PLAIN LANGUAGE:\n{str(raw_data)[:1500]}"
1611
+ )
1612
+ sources.append({"type": "raw_translation", "label": "Raw Signal", "value": str(raw_data)[:100]})
1613
+
1614
+ # Construct enriched prompt
1615
+ grounding_block = "\n".join(grounding_lines)
1616
+ translate_instruction = (
1617
+ "\n\nIMPORTANT: The user has requested plain-language translation of raw machine data. "
1618
+ "Explain all technical signals, scores, and IOCs in simple, non-technical language first, "
1619
+ "then give actionable security advice." if translate_mode else ""
1620
+ )
1621
+
1622
+ conversation_instruction = ""
1623
+ if history:
1624
+ conversation_instruction = "\n\nCONVERSATION CONTEXT (recent turns):\n" + "\n".join(
1625
+ f"{h.get('role','?').upper()}: {str(h.get('content',''))[:300]}"
1626
+ for h in history[-6:]
1627
+ )
1628
+
1629
+ enriched_message = message
1630
+ if grounding_block:
1631
+ enriched_message = (
1632
+ f"[LIVE SYSTEM CONTEXT — use this to ground your answer]\n"
1633
+ f"{grounding_block}\n"
1634
+ f"{translate_instruction}"
1635
+ f"{conversation_instruction}\n\n"
1636
+ f"[USER MESSAGE]\n{message}"
1637
+ )
1638
+
1639
+ # ── Run ML analysis on any URLs in the message (always available) ──
1640
+ import re as _re
1641
+ url_matches = _re.findall(r'https?://[^\s"\'<>]+', message)
1642
+ ml_url_context: List[str] = []
1643
+ detected_risk_score = 0.0
1644
+ detected_risk_level = "unknown"
1645
+
1646
+ if url_matches and ml_loader.ready:
1647
+ for url in url_matches[:3]:
1648
+ features = extract_url_features(url)
1649
+ preds = {name: ml_loader.predict(name, features) for name in ml_loader.MODEL_NAMES}
1650
+ scores = [p.get("threat_score", 0.2) for p in preds.values()]
1651
+ mx = max(scores) if scores else 0
1652
+ if mx > detected_risk_score:
1653
+ detected_risk_score = mx
1654
+ flagged = [n for n, p in preds.items() if p.get("prediction", 0) == 1]
1655
+ ml_url_context.append(
1656
+ f"ML scan of {url[:60]}: max_threat={mx:.2%}, flagged_by={flagged or 'none'}"
1657
+ )
1658
+ sources.append({"type": "ml_scan", "label": f"ML scan: {url[:50]}", "value": f"risk {mx:.0%}"})
1659
+
1660
+ if ml_url_context:
1661
+ enriched_message += "\n\n[ML PRE-SCAN RESULTS]\n" + "\n".join(ml_url_context)
1662
+
1663
+ if detected_risk_score > 0.8:
1664
+ detected_risk_level = "critical"
1665
+ elif detected_risk_score > 0.6:
1666
+ detected_risk_level = "high"
1667
+ elif detected_risk_score > 0.4:
1668
+ detected_risk_level = "medium"
1669
+ elif detected_risk_score > 0.0:
1670
+ detected_risk_level = "low"
1671
+
1672
+ # ── Grounding: add ML model state to context ──────────────────────
1673
+ if ml_loader.ready:
1674
+ n_models = len(ml_loader.models)
1675
+ sources.append({"type": "ml_models", "label": "ML Models", "value": f"{n_models}/4 loaded"})
1676
+
1677
+ # ── LLM cascade: Gemini → Security LLM → ML heuristic ────────────
1678
+ # Path 1: Gemini (premium, requires GEMINI_API_KEY env var)
1679
+ if gemini_service.ready:
1680
+ result = gemini_service.analyze(enriched_message, context=context, history=history)
1681
+ # Merge detected risk if ML gave a stronger signal
1682
+ if detected_risk_score > 0 and detected_risk_level != "unknown":
1683
+ ml_score_100 = round(detected_risk_score * 100, 1)
1684
+ if ml_score_100 > result.get("risk_score", 0):
1685
+ result["risk_score"] = ml_score_100
1686
+ result["risk_level"] = detected_risk_level
1687
+ result["session_id"] = session_id
1688
+ result["sources"] = sources
1689
+ return result
1690
+
1691
+ # Path 2: Security LLM via HF Inference API (Mistral-7B)
1692
+ llm_result = transformer_loader.security_chat(enriched_message, max_tokens=600)
1693
+ if llm_result.get("response") and not llm_result.get("error"):
1694
+ text = llm_result["response"]
1695
+ text_lower = text.lower()
1696
+ if "critical" in text_lower:
1697
+ risk_level, risk_score = "critical", 85.0
1698
+ elif "high" in text_lower:
1699
+ risk_level, risk_score = "high", 70.0
1700
+ elif "medium" in text_lower:
1701
+ risk_level, risk_score = "medium", 45.0
1702
+ else:
1703
+ risk_level, risk_score = "low", 20.0
1704
+ # Override with ML scan if stronger
1705
+ if detected_risk_score > 0:
1706
+ ml_score_100 = round(detected_risk_score * 100, 1)
1707
+ if ml_score_100 > risk_score:
1708
+ risk_score = ml_score_100
1709
+ risk_level = detected_risk_level
1710
+ return {
1711
+ "response": text,
1712
+ "confidence": 0.72,
1713
+ "risk_level": risk_level,
1714
+ "risk_score": risk_score,
1715
+ "model_used": llm_result.get("model_id", transformer_loader.SECURITY_LLM_REPO),
1716
+ "session_id": session_id,
1717
+ "sources": sources,
1718
+ "timestamp": datetime.utcnow().isoformat(),
1719
+ }
1720
+
1721
+ # Path 3: ML heuristic fallback (always available)
1722
+ fallback = gemini_service._fallback(enriched_message)
1723
+ # Override with ML scan risk if available
1724
+ if detected_risk_score > 0:
1725
+ ml_score_100 = round(detected_risk_score * 100, 1)
1726
+ if ml_score_100 > fallback.get("risk_score", 0):
1727
+ fallback["risk_score"] = ml_score_100
1728
+ fallback["risk_level"] = detected_risk_level
1729
+ fallback["session_id"] = session_id
1730
+ fallback["sources"] = sources
1731
+ return fallback
1732
+
1733
+
1734
+ @api.post("/api/analysis/network")
1735
+ async def api_analyze_network(request: Request):
1736
+ """Network traffic analysis – called by backend mlService.analyzeNetworkTraffic()"""
1737
+ body = await request.json()
1738
+ traffic_data = body.get("traffic_data", {})
1739
+ result = gemini_service.analyze(
1740
+ f"Analyze this network traffic for security threats:\n{json.dumps(traffic_data, indent=1)[:3000]}"
1741
+ )
1742
+ return result
1743
+
1744
+
1745
+ @api.post("/api/ai/execute-task")
1746
+ async def api_execute_task(request: Request):
1747
+ """AI task execution – called by backend mlService.executeAITask()"""
1748
+ body = await request.json()
1749
+ task_type = body.get("task_type", "")
1750
+ task_data = body.get("task_data", {})
1751
+ result = gemini_service.analyze(
1752
+ f"Execute cybersecurity task '{task_type}':\n{json.dumps(task_data, indent=1)[:3000]}"
1753
+ )
1754
+ return result
1755
+
1756
+
1757
+ @api.post("/api/browser/analyze")
1758
+ async def api_analyze_browser(request: Request):
1759
+ """Browser session analysis"""
1760
+ body = await request.json()
1761
+ session = body.get("session_data", body)
1762
+ result = gemini_service.analyze(
1763
+ f"Analyze this browser session for security threats:\n{json.dumps(session, indent=1)[:3000]}"
1764
+ )
1765
+ return result
1766
+
1767
+
1768
+ @api.post("/api/threat-feeds/analyze")
1769
+ async def api_analyze_threat_feed(request: Request):
1770
+ """Threat feed analysis"""
1771
+ body = await request.json()
1772
+ result = gemini_service.analyze(
1773
+ f"Analyze this threat feed:\n{json.dumps(body, indent=1)[:3000]}"
1774
+ )
1775
+ return result
1776
+
1777
+
1778
+ @api.post("/api/datasets/process")
1779
+ async def api_process_dataset(request: Request):
1780
+ body = await request.json()
1781
+ return {
1782
+ "status": "processed",
1783
+ "dataset_name": body.get("dataset_name", ""),
1784
+ "timestamp": datetime.utcnow().isoformat(),
1785
+ }
1786
+
1787
+
1788
+ # ============================================================================
1789
+ # GRADIO INTERFACE (UI tabs – notebooks, training, inference, models, API)
1790
+ # ============================================================================
1791
+
1792
+ def create_interface():
1793
+ with gr.Blocks(title="CyberForge AI") as demo:
1794
+ gr.Markdown("""
1795
+ # 🔐 CyberForge AI - ML Training Platform
1796
+
1797
+ Train cybersecurity ML models and run Jupyter notebooks on Hugging Face.
1798
+ """)
1799
+
1800
+ with gr.Tabs():
1801
+ # ============ NOTEBOOKS TAB ============
1802
+ with gr.TabItem("📓 Notebooks"):
1803
+ gr.Markdown("### Run ML Pipeline Notebooks\nExecute the CyberForge ML notebooks directly in the cloud.")
1804
+ with gr.Row():
1805
+ with gr.Column(scale=1):
1806
+ notebook_dropdown = gr.Dropdown(
1807
+ choices=get_available_notebooks(),
1808
+ label="Select Notebook",
1809
+ value=get_available_notebooks()[0] if get_available_notebooks() else None,
1810
+ )
1811
+ refresh_btn = gr.Button("🔄 Refresh List")
1812
+ view_btn = gr.Button("👁 View Content", variant="secondary")
1813
+ execute_btn = gr.Button("▶ Execute Notebook", variant="primary")
1814
+ gr.Markdown("### Run Single Cell")
1815
+ cell_number = gr.Number(label="Cell Number", value=1, minimum=1)
1816
+ run_cell_btn = gr.Button("Run Cell")
1817
+ with gr.Column(scale=2):
1818
+ notebook_status = gr.Markdown("Select a notebook to view or execute.")
1819
+ notebook_output = gr.Markdown("", label="Output")
1820
+
1821
+ def refresh_notebooks():
1822
+ notebooks = get_available_notebooks()
1823
+ return gr.update(choices=notebooks, value=notebooks[0] if notebooks else None)
1824
+
1825
+ refresh_btn.click(refresh_notebooks, outputs=notebook_dropdown)
1826
+ view_btn.click(read_notebook_content, inputs=notebook_dropdown, outputs=notebook_output)
1827
+ execute_btn.click(execute_notebook, inputs=notebook_dropdown, outputs=[notebook_status, notebook_output])
1828
+ run_cell_btn.click(run_notebook_cell, inputs=[notebook_dropdown, cell_number], outputs=notebook_output)
1829
+
1830
+ # ============ TRAIN MODEL TAB ============
1831
+ with gr.TabItem("🎯 Train Model"):
1832
+ gr.Markdown("### Train a Security ML Model\nUpload your dataset and train a model for threat detection.")
1833
+ with gr.Row():
1834
+ with gr.Column():
1835
+ task_dropdown = gr.Dropdown(choices=SECURITY_TASKS, label="Security Task", value="Phishing Detection")
1836
+ model_dropdown = gr.Dropdown(choices=list(MODEL_TYPES.keys()), label="Model Type", value="Random Forest")
1837
+ data_upload = gr.File(label="Upload Training Data (CSV)", file_types=[".csv"])
1838
+ train_btn = gr.Button("🚀 Train Model", variant="primary")
1839
+ with gr.Column():
1840
+ train_output = gr.Markdown("Upload data and click Train to begin.")
1841
+ model_path_output = gr.Textbox(label="Model Path", visible=False)
1842
+ metrics_output = gr.Textbox(label="Metrics JSON", visible=False)
1843
+ train_btn.click(train_model_from_data, inputs=[data_upload, model_dropdown, task_dropdown], outputs=[train_output, model_path_output, metrics_output])
1844
+
1845
+ # ============ INFERENCE TAB ============
1846
+ with gr.TabItem("🔍 Inference"):
1847
+ gr.Markdown("### Run Model Inference\nLoad a trained model and make predictions.")
1848
+ with gr.Row():
1849
+ with gr.Column():
1850
+ model_upload = gr.File(label="Upload Model (.pkl)")
1851
+ features_input = gr.Textbox(label="Features (JSON)", value='{"url_length": 50, "has_https": 1, "digit_count": 5}', lines=5)
1852
+ predict_btn = gr.Button("🎯 Predict", variant="primary")
1853
+ with gr.Column():
1854
+ prediction_output = gr.Textbox(label="Prediction Result", lines=10)
1855
+ predict_btn.click(run_inference, inputs=[model_upload, features_input], outputs=prediction_output)
1856
+
1857
+ # ============ MODELS TAB ============
1858
+ with gr.TabItem("📦 Models"):
1859
+ gr.Markdown("### Trained Models")
1860
+ models_list = gr.Markdown(list_trained_models())
1861
+ refresh_models_btn = gr.Button("🔄 Refresh")
1862
+ refresh_models_btn.click(list_trained_models, outputs=models_list)
1863
+
1864
+ # ============ API TAB ============
1865
+ with gr.TabItem("🔌 API"):
1866
+ gr.Markdown("""
1867
+ ## API Integration — v2.0
1868
+
1869
+ ### Core Endpoints (stable, used by Heroku backend)
1870
+
1871
+ | Method | Endpoint | Description |
1872
+ |--------|----------|-------------|
1873
+ | GET | `/health` | Health check — version, models, services |
1874
+ | POST | `/analyze` | AI chat / general analysis (Gemini or ML fallback) |
1875
+ | POST | `/analyze-url` | URL threat analysis — 4 ML models + risk factors |
1876
+ | POST | `/scan-threats` | Deep threat scan (Gemini or ML fallback) |
1877
+ | POST | `/api/insights/generate` | AI-generated security insights |
1878
+ | POST | `/api/models/predict` | Direct ML model prediction |
1879
+ | GET | `/models` | List loaded models with source info |
1880
+
1881
+ ### v2 Endpoints (new — for distributed agents + AI Deep Scan)
1882
+
1883
+ | Method | Endpoint | Description |
1884
+ |--------|----------|-------------|
1885
+ | POST | `/api/v2/batch-analyze` | Batch analyze up to 20 URLs (for multi-agent) |
1886
+ | GET | `/api/v2/explain/{model_name}` | Feature importances / explainability |
1887
+ | POST | `/api/v2/url-classify` | BERT-based URL phishing classifier |
1888
+ | POST | `/api/v2/dga-detect` | DGA domain detection (entropy heuristic) |
1889
+ | POST | `/api/v2/ioc-scan` | Multi-IOC scanner: URL + domain + DGA + BERT |
1890
+ | POST | `/api/v2/url-enrich` | Enriched analysis: ML + BERT + DGA + explainability |
1891
+ | POST | `/api/v2/chat` | Security chatbot — context-grounded, RAG-lite, machine→human translation |
1892
+ | POST | `/api/v2/security-chat` | Security LLM Q&A (Mistral-7B via HF Inference API) |
1893
+ | GET | `/api/v2/status` | Transformer model load status |
1894
+
1895
+ ### Quick Examples
1896
+
1897
+ ```bash
1898
+ # Batch analyze URLs (for distributed agents)
1899
+ curl -X POST https://che237-cyberforge.hf.space/api/v2/batch-analyze \\
1900
+ -H "Content-Type: application/json" \\
1901
+ -d '{"urls": ["https://example.com", "http://evil.xyz/login?id=1"]}'
1902
+
1903
+ # Enriched URL analysis (Deep Scan mode)
1904
+ curl -X POST https://che237-cyberforge.hf.space/api/v2/url-enrich \\
1905
+ -H "Content-Type: application/json" \\
1906
+ -d '{"url": "https://example.com"}'
1907
+
1908
+ # Feature explainability
1909
+ curl https://che237-cyberforge.hf.space/api/v2/explain/phishing_detection
1910
+
1911
+ # IOC multi-field scan
1912
+ curl -X POST https://che237-cyberforge.hf.space/api/v2/ioc-scan \\
1913
+ -H "Content-Type: application/json" \\
1914
+ -d '{"url": "http://192.168.1.1/login", "domain": "192.168.1.1"}'
1915
+
1916
+ # Chatbot (Wave 2 extension point)
1917
+ curl -X POST https://che237-cyberforge.hf.space/api/v2/chat \\
1918
+ -H "Content-Type: application/json" \\
1919
+ -d '{"message": "Is this URL suspicious: http://login.g00gle.com.xyz/verify"}'
1920
+ ```
1921
+ """)
1922
+
1923
+ gr.Markdown("---\n**CyberForge AI** | [GitHub](https://github.com/Che237/cyberforge) | [Datasets](https://huggingface.co/datasets/Che237/cyberforge-datasets)")
1924
+
1925
+ return demo
1926
+
1927
+
1928
+ # ============================================================================
1929
+ # MAIN – Mount Gradio on FastAPI
1930
+ # ============================================================================
1931
+
1932
+ # Initialize services at startup
1933
+ logger.info("🚀 Initializing CyberForge AI services...")
1934
+ gemini_service.initialize()
1935
+ ml_loader.initialize()
1936
+ logger.info("✅ Services initialized")
1937
+
1938
+ # Create Gradio app and mount it on FastAPI
1939
+ demo = create_interface()
1940
+ app = gr.mount_gradio_app(api, demo, path="/")
1941
+
1942
+ if __name__ == "__main__":
1943
+ import uvicorn
1944
+ port = int(os.environ.get("PORT", 7860))
1945
+ uvicorn.run(app, host="0.0.0.0", port=port)
requirements.txt ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CyberForge AI - HuggingFace Space
2
+ # Combined Gradio UI + FastAPI REST API
3
+
4
+ # Core ML
5
+ scikit-learn>=1.5.0
6
+ pandas>=2.2.0
7
+ numpy>=2.0.0
8
+ joblib>=1.4.0
9
+
10
+ # Transformers (for phase-3 BERT URL classifier + DGA detector)
11
+ torch>=2.2.0
12
+ transformers>=4.40.0
13
+ tokenizers>=0.19.0
14
+
15
+ # Gradio UI
16
+ gradio>=5.20.0
17
+
18
+ # FastAPI REST endpoints (for Heroku backend)
19
+ fastapi>=0.100.0
20
+ uvicorn[standard]>=0.22.0
21
+
22
+ # HF Hub
23
+ huggingface_hub>=0.25.0
24
+
25
+ # Feature Engineering
26
+ tldextract>=5.1.0
27
+
28
+ # Gemini AI (new SDK)
29
+ google-genai>=1.0.0
30
+
31
+ # Notebook execution
32
+ nbformat>=5.10.0
33
+ nbconvert>=7.16.0
34
+ nest_asyncio>=1.6.0
35
+
36
+ # HTTP client for notebooks
37
+ httpx>=0.27.0
38
+
39
+ # Data handling
40
+ datasets>=2.20.0
41
+ pyarrow>=17.0.0
42
+
43
+ # Utilities
44
+ requests>=2.31.0
45
+ python-dotenv>=1.0.0