Spaces:
Running
Running
Replace Gemini with in-Space open-source LLM (Qwen3.5-2B via llama.cpp)
Browse files- Dockerfile +1 -0
- app.py +313 -218
- requirements.txt +10 -4
Dockerfile
CHANGED
|
@@ -10,6 +10,7 @@ RUN apt-get update && apt-get install -y \
|
|
| 10 |
ffmpeg \
|
| 11 |
libsm6 \
|
| 12 |
libxext6 \
|
|
|
|
| 13 |
&& rm -rf /var/lib/apt/lists/* \
|
| 14 |
&& git lfs install
|
| 15 |
|
|
|
|
| 10 |
ffmpeg \
|
| 11 |
libsm6 \
|
| 12 |
libxext6 \
|
| 13 |
+
libgomp1 \
|
| 14 |
&& rm -rf /var/lib/apt/lists/* \
|
| 15 |
&& git lfs install
|
| 16 |
|
app.py
CHANGED
|
@@ -12,6 +12,8 @@ import json
|
|
| 12 |
import os
|
| 13 |
import subprocess
|
| 14 |
import sys
|
|
|
|
|
|
|
| 15 |
from pathlib import Path
|
| 16 |
from datetime import datetime
|
| 17 |
import logging
|
|
@@ -33,13 +35,7 @@ from huggingface_hub import HfApi, hf_hub_download, upload_file
|
|
| 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__)
|
|
@@ -60,11 +56,22 @@ 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()}")
|
|
@@ -86,131 +93,50 @@ SECURITY_TASKS = [
|
|
| 86 |
|
| 87 |
|
| 88 |
# ============================================================================
|
| 89 |
-
#
|
| 90 |
# ============================================================================
|
| 91 |
|
| 92 |
-
class
|
| 93 |
-
"""
|
| 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 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
4. Detailed technical analysis
|
| 107 |
-
5. Specific actionable recommendations
|
| 108 |
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 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 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 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
|
| 214 |
import re
|
| 215 |
global ml_loader
|
| 216 |
|
|
@@ -313,11 +239,11 @@ Provide a comprehensive cybersecurity analysis:"""
|
|
| 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
|
| 317 |
)
|
| 318 |
insights = [f"{n_loaded} ML models active"]
|
| 319 |
|
| 320 |
-
response_lines.append("\n---\n*Powered by CyberForge ML models —
|
| 321 |
|
| 322 |
return {
|
| 323 |
"response": "\n".join(response_lines),
|
|
@@ -332,7 +258,7 @@ Provide a comprehensive cybersecurity analysis:"""
|
|
| 332 |
|
| 333 |
|
| 334 |
# Singleton
|
| 335 |
-
|
| 336 |
|
| 337 |
|
| 338 |
# ============================================================================
|
|
@@ -567,8 +493,149 @@ def extract_url_features(url: str) -> Dict:
|
|
| 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 |
-
#
|
| 571 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 572 |
|
| 573 |
class TransformerModelLoader:
|
| 574 |
"""Loads pretrained Transformer classifiers from the HF Hub on demand."""
|
|
@@ -585,14 +652,17 @@ class TransformerModelLoader:
|
|
| 585 |
"desc": "BERT-based URL phishing/malware classifier",
|
| 586 |
},
|
| 587 |
}
|
| 588 |
-
#
|
| 589 |
-
#
|
| 590 |
-
#
|
| 591 |
-
|
|
|
|
| 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
|
|
@@ -694,53 +764,83 @@ class TransformerModelLoader:
|
|
| 694 |
"inference_source": "entropy-heuristic",
|
| 695 |
}
|
| 696 |
|
| 697 |
-
|
| 698 |
-
"
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 707 |
try:
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 717 |
if r.status_code == 200:
|
| 718 |
data = r.json()
|
| 719 |
-
text = data[0]
|
| 720 |
return {
|
| 721 |
-
"model": "
|
| 722 |
-
"source": "
|
| 723 |
"response": text,
|
| 724 |
-
"model_id": self.
|
| 725 |
}
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
"fallback_available": gemini_service.ready,
|
| 730 |
-
}
|
| 731 |
except Exception as e:
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 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()) + ["
|
| 743 |
"load_errors": self.load_errors,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 744 |
}
|
| 745 |
|
| 746 |
@staticmethod
|
|
@@ -1027,7 +1127,9 @@ async def api_health():
|
|
| 1027 |
"status": "healthy",
|
| 1028 |
"timestamp": datetime.utcnow().isoformat(),
|
| 1029 |
"services": {
|
| 1030 |
-
"
|
|
|
|
|
|
|
| 1031 |
"ml_models": ml_loader.ready,
|
| 1032 |
"models_loaded": list(ml_loader.models.keys()),
|
| 1033 |
"transformers": transformer_loader.transformers_available,
|
|
@@ -1053,7 +1155,7 @@ async def api_analyze(request: Request):
|
|
| 1053 |
query = body.get("query", "")
|
| 1054 |
context = body.get("context", {})
|
| 1055 |
history = body.get("conversation_history", [])
|
| 1056 |
-
result =
|
| 1057 |
return result
|
| 1058 |
|
| 1059 |
|
|
@@ -1131,7 +1233,7 @@ 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 =
|
| 1135 |
return result
|
| 1136 |
|
| 1137 |
|
|
@@ -1141,7 +1243,7 @@ async def api_generate_insights(request: Request):
|
|
| 1141 |
body = await request.json()
|
| 1142 |
query = body.get("query", "")
|
| 1143 |
context = body.get("context", {})
|
| 1144 |
-
result =
|
| 1145 |
return {
|
| 1146 |
"insights": result.get("response", ""),
|
| 1147 |
"confidence": result.get("confidence", 0),
|
|
@@ -1200,20 +1302,18 @@ async def api_v2_dga_detect(request: Request):
|
|
| 1200 |
|
| 1201 |
@api.post("/api/v2/security-chat")
|
| 1202 |
async def api_v2_security_chat(request: Request):
|
| 1203 |
-
"""Cybersecurity Q&A
|
|
|
|
| 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
|
| 1211 |
-
#
|
| 1212 |
-
if result.get("error")
|
| 1213 |
-
|
| 1214 |
-
gemini_result["source"] = "gemini-fallback"
|
| 1215 |
-
gemini_result["llm_error"] = result["error"]
|
| 1216 |
-
return gemini_result
|
| 1217 |
return result
|
| 1218 |
|
| 1219 |
|
|
@@ -1324,7 +1424,7 @@ async def api_v2_ioc_scan(request: Request):
|
|
| 1324 |
"domain": "example.com", # optional
|
| 1325 |
"ip": "1.2.3.4", # optional
|
| 1326 |
"hash": "abc123...", # optional (future use)
|
| 1327 |
-
"context": { ... } # optional — passed to
|
| 1328 |
}
|
| 1329 |
|
| 1330 |
Response: per-indicator results with unified risk summary.
|
|
@@ -1382,13 +1482,15 @@ async def api_v2_ioc_scan(request: Request):
|
|
| 1382 |
else:
|
| 1383 |
risk_level = "unknown"
|
| 1384 |
|
| 1385 |
-
# Optional
|
|
|
|
| 1386 |
ai_analysis = None
|
| 1387 |
-
if
|
| 1388 |
target = url or domain
|
| 1389 |
-
ai_analysis =
|
|
|
|
| 1390 |
f"Scan these IOC indicators for threats: url={target}, domain={effective_domain}",
|
| 1391 |
-
context
|
| 1392 |
)
|
| 1393 |
|
| 1394 |
return {
|
|
@@ -1506,8 +1608,9 @@ async def api_v2_chat(request: Request):
|
|
| 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.
|
| 1510 |
-
|
|
|
|
| 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
|
|
@@ -1674,22 +1777,9 @@ async def api_v2_chat(request: Request):
|
|
| 1674 |
n_models = len(ml_loader.models)
|
| 1675 |
sources.append({"type": "ml_models", "label": "ML Models", "value": f"{n_models}/4 loaded"})
|
| 1676 |
|
| 1677 |
-
#
|
| 1678 |
-
#
|
| 1679 |
-
|
| 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()
|
|
@@ -1712,14 +1802,14 @@ async def api_v2_chat(request: Request):
|
|
| 1712 |
"confidence": 0.72,
|
| 1713 |
"risk_level": risk_level,
|
| 1714 |
"risk_score": risk_score,
|
| 1715 |
-
"model_used": llm_result.get("model_id",
|
| 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 =
|
| 1723 |
# Override with ML scan risk if available
|
| 1724 |
if detected_risk_score > 0:
|
| 1725 |
ml_score_100 = round(detected_risk_score * 100, 1)
|
|
@@ -1736,8 +1826,9 @@ 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 =
|
| 1740 |
-
|
|
|
|
| 1741 |
)
|
| 1742 |
return result
|
| 1743 |
|
|
@@ -1748,8 +1839,9 @@ async def api_execute_task(request: Request):
|
|
| 1748 |
body = await request.json()
|
| 1749 |
task_type = body.get("task_type", "")
|
| 1750 |
task_data = body.get("task_data", {})
|
| 1751 |
-
result =
|
| 1752 |
-
|
|
|
|
| 1753 |
)
|
| 1754 |
return result
|
| 1755 |
|
|
@@ -1759,8 +1851,9 @@ 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 =
|
| 1763 |
-
|
|
|
|
| 1764 |
)
|
| 1765 |
return result
|
| 1766 |
|
|
@@ -1769,8 +1862,9 @@ async def api_analyze_browser(request: Request):
|
|
| 1769 |
async def api_analyze_threat_feed(request: Request):
|
| 1770 |
"""Threat feed analysis"""
|
| 1771 |
body = await request.json()
|
| 1772 |
-
result =
|
| 1773 |
-
|
|
|
|
| 1774 |
)
|
| 1775 |
return result
|
| 1776 |
|
|
@@ -1871,9 +1965,9 @@ def create_interface():
|
|
| 1871 |
| Method | Endpoint | Description |
|
| 1872 |
|--------|----------|-------------|
|
| 1873 |
| GET | `/health` | Health check — version, models, services |
|
| 1874 |
-
| POST | `/analyze` | AI chat / general analysis (
|
| 1875 |
| POST | `/analyze-url` | URL threat analysis — 4 ML models + risk factors |
|
| 1876 |
-
| POST | `/scan-threats` | Deep threat scan (
|
| 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 |
|
|
@@ -1889,7 +1983,7 @@ def create_interface():
|
|
| 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 (
|
| 1893 |
| GET | `/api/v2/status` | Transformer model load status |
|
| 1894 |
|
| 1895 |
### Quick Examples
|
|
@@ -1931,8 +2025,9 @@ curl -X POST https://che237-cyberforge.hf.space/api/v2/chat \\
|
|
| 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
|
|
|
|
| 12 |
import os
|
| 13 |
import subprocess
|
| 14 |
import sys
|
| 15 |
+
import threading
|
| 16 |
+
import time
|
| 17 |
from pathlib import Path
|
| 18 |
from datetime import datetime
|
| 19 |
import logging
|
|
|
|
| 35 |
from fastapi import FastAPI, Request
|
| 36 |
from fastapi.middleware.cors import CORSMiddleware
|
| 37 |
from fastapi.responses import JSONResponse
|
| 38 |
+
from starlette.concurrency import run_in_threadpool
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
logging.basicConfig(level=logging.INFO)
|
| 41 |
logger = logging.getLogger(__name__)
|
|
|
|
| 56 |
TRAINING_DATA_DIR.mkdir(exist_ok=True)
|
| 57 |
|
| 58 |
# Environment
|
|
|
|
|
|
|
| 59 |
HF_TOKEN = os.environ.get("HF_TOKEN", "")
|
| 60 |
HF_MODEL_REPO = os.environ.get("HF_MODEL_REPO", "Che237/cyberforge-models")
|
| 61 |
|
| 62 |
+
# Open-source chat model that runs inside this Space on CPU (llama.cpp, GGUF).
|
| 63 |
+
# It replaces Gemini and needs no API key or credit. Any GGUF chat model can be
|
| 64 |
+
# set here; on the free cpu-basic tier (2 vCPU) stay around 2B parameters or the
|
| 65 |
+
# answers will not fit inside the callers' timeouts.
|
| 66 |
+
LOCAL_LLM_REPO = os.environ.get("LOCAL_LLM_REPO", "unsloth/Qwen3.5-2B-GGUF")
|
| 67 |
+
LOCAL_LLM_FILE = os.environ.get("LOCAL_LLM_FILE", "Qwen3.5-2B-Q4_K_M.gguf")
|
| 68 |
+
LOCAL_LLM_CTX = int(os.environ.get("LOCAL_LLM_CTX", "4096"))
|
| 69 |
+
LOCAL_LLM_THREADS = int(os.environ.get("LOCAL_LLM_THREADS", "2"))
|
| 70 |
+
LOCAL_LLM_MAX_TOKENS = int(os.environ.get("LOCAL_LLM_MAX_TOKENS", "320"))
|
| 71 |
+
LOCAL_LLM_MAX_INPUT_CHARS = int(os.environ.get("LOCAL_LLM_MAX_INPUT_CHARS", "2400"))
|
| 72 |
+
LOCAL_LLM_TIME_BUDGET_S = float(os.environ.get("LOCAL_LLM_TIME_BUDGET_S", "22"))
|
| 73 |
+
LOCAL_LLM_QUEUE_TIMEOUT_S = float(os.environ.get("LOCAL_LLM_QUEUE_TIMEOUT_S", "5"))
|
| 74 |
+
|
| 75 |
logger.info(f"APP_DIR: {APP_DIR}")
|
| 76 |
logger.info(f"NOTEBOOKS_DIR: {NOTEBOOKS_DIR}")
|
| 77 |
logger.info(f"NOTEBOOKS_DIR exists: {NOTEBOOKS_DIR.exists()}")
|
|
|
|
| 93 |
|
| 94 |
|
| 95 |
# ============================================================================
|
| 96 |
+
# REASONING SERVICE (LLM-backed analysis for the REST API)
|
| 97 |
# ============================================================================
|
| 98 |
|
| 99 |
+
class ReasoningService:
|
| 100 |
+
"""Structured cybersecurity analysis for the REST endpoints.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
|
| 102 |
+
The text comes from TransformerModelLoader.security_chat: the hosted model
|
| 103 |
+
when the account has Inference Providers credit, otherwise the open-source
|
| 104 |
+
model running inside this Space. If no LLM answers, the ML/keyword heuristic
|
| 105 |
+
in _fallback is used."""
|
|
|
|
|
|
|
| 106 |
|
| 107 |
+
def analyze(self, query: str, context: Dict = None, history: List = None,
|
| 108 |
+
allow_local: bool = True) -> Dict:
|
| 109 |
+
tl = globals().get("transformer_loader")
|
| 110 |
+
if tl is None:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
return self._fallback(query)
|
| 112 |
+
ctx = f"\n\nCONTEXT:\n{json.dumps(context, indent=1)[:2000]}" if context else ""
|
| 113 |
+
llm = tl.security_chat(f"{query}{ctx}", max_tokens=700, allow_local=allow_local)
|
| 114 |
+
text = llm.get("response")
|
| 115 |
+
if not text or llm.get("error"):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
return self._fallback(query)
|
| 117 |
+
tlow = text.lower()
|
| 118 |
+
if "critical" in tlow:
|
| 119 |
+
risk_level, risk_score = "Critical", 9.0
|
| 120 |
+
elif "high" in tlow:
|
| 121 |
+
risk_level, risk_score = "High", 7.0
|
| 122 |
+
elif "medium" in tlow:
|
| 123 |
+
risk_level, risk_score = "Medium", 5.0
|
| 124 |
+
else:
|
| 125 |
+
risk_level, risk_score = "Low", 3.0
|
| 126 |
+
model_id = llm.get("model_id", "llm")
|
| 127 |
+
return {
|
| 128 |
+
"response": text,
|
| 129 |
+
"confidence": 0.82,
|
| 130 |
+
"risk_level": risk_level,
|
| 131 |
+
"risk_score": risk_score,
|
| 132 |
+
"insights": [f"Analysis by {model_id} via {llm.get('source', 'llm')}"],
|
| 133 |
+
"recommendations": [],
|
| 134 |
+
"model_used": model_id,
|
| 135 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 136 |
+
}
|
| 137 |
|
| 138 |
def _fallback(self, query: str) -> Dict:
|
| 139 |
+
"""ML-powered analysis when no LLM is available"""
|
| 140 |
import re
|
| 141 |
global ml_loader
|
| 142 |
|
|
|
|
| 239 |
"- URL threat analysis (phishing, malware, web attacks)\n"
|
| 240 |
"- Network anomaly detection\n"
|
| 241 |
"- Real-time threat event monitoring\n\n"
|
| 242 |
+
"*The AI chat model is currently unavailable. Provide a URL for full ML-based analysis.*"
|
| 243 |
)
|
| 244 |
insights = [f"{n_loaded} ML models active"]
|
| 245 |
|
| 246 |
+
response_lines.append("\n---\n*Powered by CyberForge ML models — AI chat model offline.*")
|
| 247 |
|
| 248 |
return {
|
| 249 |
"response": "\n".join(response_lines),
|
|
|
|
| 258 |
|
| 259 |
|
| 260 |
# Singleton
|
| 261 |
+
reasoning_service = ReasoningService()
|
| 262 |
|
| 263 |
|
| 264 |
# ============================================================================
|
|
|
|
| 493 |
#
|
| 494 |
# Loads pretrained BERT-based classifiers from the Hugging Face Hub.
|
| 495 |
# Models are loaded lazily on first request to keep cold-start fast.
|
| 496 |
+
# The chat LLM is separate: see LocalLLM below (runs in this Space) and
|
| 497 |
+
# TransformerModelLoader.security_chat (hosted model first, then LocalLLM).
|
| 498 |
+
|
| 499 |
+
class LocalLLM:
|
| 500 |
+
"""Open-source chat model served from inside the Space with llama.cpp.
|
| 501 |
+
|
| 502 |
+
The GGUF file is downloaded from the Hub once and kept in memory. Generation
|
| 503 |
+
is serialized (a llama.cpp context is not thread-safe) and time-boxed, so a
|
| 504 |
+
slow CPU cannot hold a request past the callers' timeouts."""
|
| 505 |
+
|
| 506 |
+
def __init__(self):
|
| 507 |
+
self.llm = None
|
| 508 |
+
self.error: Optional[str] = None
|
| 509 |
+
self._chatml = False
|
| 510 |
+
self._prefill = ""
|
| 511 |
+
self._load_lock = threading.Lock()
|
| 512 |
+
self._gen_lock = threading.Lock()
|
| 513 |
+
|
| 514 |
+
@property
|
| 515 |
+
def ready(self) -> bool:
|
| 516 |
+
return self.llm is not None
|
| 517 |
+
|
| 518 |
+
def status(self) -> Dict:
|
| 519 |
+
return {
|
| 520 |
+
"repo": LOCAL_LLM_REPO,
|
| 521 |
+
"file": LOCAL_LLM_FILE,
|
| 522 |
+
"loaded": self.ready,
|
| 523 |
+
"loading": self._load_lock.locked(),
|
| 524 |
+
"error": self.error,
|
| 525 |
+
}
|
| 526 |
+
|
| 527 |
+
def load(self):
|
| 528 |
+
"""Download and load the model. Safe to call repeatedly."""
|
| 529 |
+
with self._load_lock:
|
| 530 |
+
if self.llm is not None or self.error:
|
| 531 |
+
return self.llm
|
| 532 |
+
try:
|
| 533 |
+
from llama_cpp import Llama
|
| 534 |
+
try:
|
| 535 |
+
path = hf_hub_download(repo_id=LOCAL_LLM_REPO, filename=LOCAL_LLM_FILE, token=HF_TOKEN or None)
|
| 536 |
+
except Exception:
|
| 537 |
+
# A stale HF_TOKEN must not block the download of a public model
|
| 538 |
+
path = hf_hub_download(repo_id=LOCAL_LLM_REPO, filename=LOCAL_LLM_FILE, token=False)
|
| 539 |
+
self._probe(path)
|
| 540 |
+
llm = Llama(model_path=path, n_ctx=LOCAL_LLM_CTX, n_threads=LOCAL_LLM_THREADS, verbose=False)
|
| 541 |
+
template = str((llm.metadata or {}).get("tokenizer.chat_template", ""))
|
| 542 |
+
self._chatml = "<|im_start|>" in template
|
| 543 |
+
# Qwen3.5 answers directly when the assistant turn opens with an
|
| 544 |
+
# empty think block; this is what its template emits for
|
| 545 |
+
# enable_thinking=False.
|
| 546 |
+
self._prefill = "<think>\n\n</think>\n\n" if "<think>" in template else ""
|
| 547 |
+
self.llm = llm
|
| 548 |
+
logger.info(f"✅ Local LLM loaded: {LOCAL_LLM_REPO}/{LOCAL_LLM_FILE}")
|
| 549 |
+
except Exception as e:
|
| 550 |
+
self.error = f"{type(e).__name__}: {str(e)[:300]}"
|
| 551 |
+
logger.error(f"❌ Local LLM unavailable: {self.error}")
|
| 552 |
+
return self.llm
|
| 553 |
+
|
| 554 |
+
@staticmethod
|
| 555 |
+
def _probe(path: str) -> None:
|
| 556 |
+
"""Load the model once in a child process. A native crash there (for
|
| 557 |
+
example a CPU instruction the prebuilt wheel needs but this machine
|
| 558 |
+
lacks) raises here instead of killing the API process."""
|
| 559 |
+
code = (
|
| 560 |
+
"import sys; from llama_cpp import Llama; "
|
| 561 |
+
"m = Llama(model_path=sys.argv[1], n_ctx=256, n_threads=1, verbose=False); "
|
| 562 |
+
"m.create_completion('Hi', max_tokens=1)"
|
| 563 |
+
)
|
| 564 |
+
r = subprocess.run([sys.executable, "-c", code, path], capture_output=True, text=True, timeout=300)
|
| 565 |
+
if r.returncode != 0:
|
| 566 |
+
raise RuntimeError(f"llama.cpp probe exited with {r.returncode}: {(r.stderr or '')[-200:]}")
|
| 567 |
+
|
| 568 |
+
def chat(self, system: str, user: str, max_tokens: int = 512) -> Dict:
|
| 569 |
+
base = {"model": "local-llm", "source": "space_local_llamacpp", "model_id": LOCAL_LLM_REPO}
|
| 570 |
+
if self.llm is None:
|
| 571 |
+
if self._load_lock.locked():
|
| 572 |
+
return {**base, "response": None, "error": "local LLM is still loading"}
|
| 573 |
+
self.load()
|
| 574 |
+
if self.llm is None:
|
| 575 |
+
return {**base, "response": None, "error": self.error or "local LLM not loaded"}
|
| 576 |
+
if not self._gen_lock.acquire(timeout=LOCAL_LLM_QUEUE_TIMEOUT_S):
|
| 577 |
+
return {**base, "response": None, "error": "local LLM is busy with another request"}
|
| 578 |
+
started = time.monotonic()
|
| 579 |
+
stream = None
|
| 580 |
+
try:
|
| 581 |
+
max_tokens = max(16, min(int(max_tokens), LOCAL_LLM_MAX_TOKENS))
|
| 582 |
+
# The chat markers are structural: drop them from untrusted text
|
| 583 |
+
system = system.replace("<|im_start|>", "").replace("<|im_end|>", "")
|
| 584 |
+
user = user.replace("<|im_start|>", "").replace("<|im_end|>", "")
|
| 585 |
+
if len(user) > LOCAL_LLM_MAX_INPUT_CHARS:
|
| 586 |
+
# Keep both ends: context sits at the start, the question at the end
|
| 587 |
+
half = LOCAL_LLM_MAX_INPUT_CHARS // 2
|
| 588 |
+
user = f"{user[:half]}\n[...]\n{user[-half:]}"
|
| 589 |
+
if self._chatml:
|
| 590 |
+
prompt = (
|
| 591 |
+
f"<|im_start|>system\n{system}<|im_end|>\n"
|
| 592 |
+
f"<|im_start|>user\n{user}<|im_end|>\n"
|
| 593 |
+
f"<|im_start|>assistant\n{self._prefill}"
|
| 594 |
+
)
|
| 595 |
+
stream = self.llm.create_completion(
|
| 596 |
+
prompt, max_tokens=max_tokens, temperature=0.7, top_p=0.8, top_k=20,
|
| 597 |
+
presence_penalty=1.5, stop=["<|im_end|>", "<|endoftext|>"], stream=True,
|
| 598 |
+
)
|
| 599 |
+
read = lambda c: c["choices"][0].get("text") or ""
|
| 600 |
+
else:
|
| 601 |
+
stream = self.llm.create_chat_completion(
|
| 602 |
+
messages=[{"role": "system", "content": system}, {"role": "user", "content": user}],
|
| 603 |
+
max_tokens=max_tokens, temperature=0.7, top_p=0.8, stream=True,
|
| 604 |
+
)
|
| 605 |
+
read = lambda c: c["choices"][0].get("delta", {}).get("content") or ""
|
| 606 |
+
pieces: List[str] = []
|
| 607 |
+
truncated = False
|
| 608 |
+
for chunk in stream:
|
| 609 |
+
pieces.append(read(chunk))
|
| 610 |
+
if time.monotonic() - started > LOCAL_LLM_TIME_BUDGET_S:
|
| 611 |
+
truncated = True
|
| 612 |
+
break
|
| 613 |
+
import re
|
| 614 |
+
text = re.sub(r"<think>.*?</think>", "", "".join(pieces), flags=re.S).strip()
|
| 615 |
+
if truncated:
|
| 616 |
+
# Out of time: end on the last complete sentence or line
|
| 617 |
+
cut = max(text.rfind(". "), text.rfind(".\n"), text.rfind("\n"))
|
| 618 |
+
if cut > len(text) * 0.5:
|
| 619 |
+
text = text[:cut + 1].strip()
|
| 620 |
+
if not text:
|
| 621 |
+
return {**base, "response": None, "error": "local LLM returned no text"}
|
| 622 |
+
return {
|
| 623 |
+
**base,
|
| 624 |
+
"response": text,
|
| 625 |
+
"tokens": len(pieces),
|
| 626 |
+
"elapsed_ms": round((time.monotonic() - started) * 1000),
|
| 627 |
+
"truncated": truncated,
|
| 628 |
+
}
|
| 629 |
+
except Exception as e:
|
| 630 |
+
return {**base, "response": None, "error": f"{type(e).__name__}: {str(e)[:200]}"}
|
| 631 |
+
finally:
|
| 632 |
+
if stream is not None and hasattr(stream, "close"):
|
| 633 |
+
stream.close()
|
| 634 |
+
self._gen_lock.release()
|
| 635 |
+
|
| 636 |
+
|
| 637 |
+
local_llm = LocalLLM()
|
| 638 |
+
|
| 639 |
|
| 640 |
class TransformerModelLoader:
|
| 641 |
"""Loads pretrained Transformer classifiers from the HF Hub on demand."""
|
|
|
|
| 652 |
"desc": "BERT-based URL phishing/malware classifier",
|
| 653 |
},
|
| 654 |
}
|
| 655 |
+
# Hosted reasoning model through HF Inference Providers (OpenAI-compatible
|
| 656 |
+
# router at router.huggingface.co/v1). Billed per token, so it only answers
|
| 657 |
+
# while the account has credit; otherwise security_chat uses LocalLLM.
|
| 658 |
+
# Override via REASONING_LLM_MODEL.
|
| 659 |
+
REASONING_LLM_REPO = os.environ.get("REASONING_LLM_MODEL", "deepseek-ai/DeepSeek-V3-0324")
|
| 660 |
|
| 661 |
def __init__(self):
|
| 662 |
self.pipelines = {} # name → transformers.Pipeline (lazy-loaded)
|
| 663 |
self.load_errors = {} # name → last error message
|
| 664 |
+
self.last_engine = None # "hosted" | "local": which LLM answered last
|
| 665 |
+
self._provider_retry_at = 0.0 # monotonic time before which the hosted model is skipped
|
| 666 |
self.transformers_available = False
|
| 667 |
try:
|
| 668 |
import transformers # noqa: F401
|
|
|
|
| 764 |
"inference_source": "entropy-heuristic",
|
| 765 |
}
|
| 766 |
|
| 767 |
+
SYSTEM_PROMPT = (
|
| 768 |
+
"You are CyberForge AI, a precise, professional cybersecurity analyst. "
|
| 769 |
+
"Give concise, technical, actionable answers. State a Risk Level "
|
| 770 |
+
"(Critical/High/Medium/Low) when assessing a threat, and cite CVE / MITRE "
|
| 771 |
+
"ATT&CK IDs where relevant."
|
| 772 |
+
)
|
| 773 |
+
|
| 774 |
+
def provider_available(self) -> bool:
|
| 775 |
+
"""True when the hosted model is worth trying: a token is set and the
|
| 776 |
+
last call did not fail on billing or auth."""
|
| 777 |
+
return bool(HF_TOKEN) and time.monotonic() >= self._provider_retry_at
|
| 778 |
+
|
| 779 |
+
def _provider_chat(self, query: str, max_tokens: int) -> Optional[Dict]:
|
| 780 |
+
"""Ask the hosted model. Returns None when it cannot answer."""
|
| 781 |
+
import requests
|
| 782 |
try:
|
| 783 |
+
r = requests.post(
|
| 784 |
+
"https://router.huggingface.co/v1/chat/completions",
|
| 785 |
+
headers={"Authorization": f"Bearer {HF_TOKEN}", "Content-Type": "application/json"},
|
| 786 |
+
json={
|
| 787 |
+
"model": self.REASONING_LLM_REPO,
|
| 788 |
+
"messages": [
|
| 789 |
+
{"role": "system", "content": self.SYSTEM_PROMPT},
|
| 790 |
+
{"role": "user", "content": query[:6000]},
|
| 791 |
+
],
|
| 792 |
+
"max_tokens": max_tokens,
|
| 793 |
+
"temperature": 0.3,
|
| 794 |
+
},
|
| 795 |
+
timeout=60,
|
| 796 |
+
)
|
| 797 |
if r.status_code == 200:
|
| 798 |
data = r.json()
|
| 799 |
+
text = data["choices"][0]["message"]["content"]
|
| 800 |
return {
|
| 801 |
+
"model": "deepseek",
|
| 802 |
+
"source": "hf_inference_providers",
|
| 803 |
"response": text,
|
| 804 |
+
"model_id": self.REASONING_LLM_REPO,
|
| 805 |
}
|
| 806 |
+
# 401/402/403 (bad token, no credit) will not fix themselves quickly
|
| 807 |
+
self._provider_retry_at = time.monotonic() + (900 if r.status_code in (401, 402, 403) else 60)
|
| 808 |
+
logger.warning(f"Hosted LLM HTTP {r.status_code}: {r.text[:160]} — using the local model")
|
|
|
|
|
|
|
| 809 |
except Exception as e:
|
| 810 |
+
self._provider_retry_at = time.monotonic() + 60
|
| 811 |
+
logger.warning(f"Hosted LLM error: {type(e).__name__}: {str(e)[:160]} — using the local model")
|
| 812 |
+
return None
|
| 813 |
+
|
| 814 |
+
def security_chat(self, query: str, max_tokens: int = 512, allow_local: bool = True) -> Dict:
|
| 815 |
+
"""Cybersecurity Q&A.
|
| 816 |
+
1. Hosted model through HF Inference Providers, while the account has credit.
|
| 817 |
+
2. The open-source model running inside this Space (free, no key)."""
|
| 818 |
+
if self.provider_available():
|
| 819 |
+
hosted = self._provider_chat(query, max_tokens)
|
| 820 |
+
if hosted:
|
| 821 |
+
self.last_engine = "hosted"
|
| 822 |
+
return hosted
|
| 823 |
+
if not allow_local:
|
| 824 |
+
return {"model": "security-llm", "response": None, "error": "hosted LLM unavailable"}
|
| 825 |
+
result = local_llm.chat(self.SYSTEM_PROMPT, query, max_tokens=max_tokens)
|
| 826 |
+
if result.get("response"):
|
| 827 |
+
self.last_engine = "local"
|
| 828 |
+
return result
|
| 829 |
|
| 830 |
def status(self) -> Dict:
|
| 831 |
return {
|
| 832 |
"transformers_available": self.transformers_available,
|
| 833 |
"loaded": list(self.pipelines.keys()),
|
| 834 |
+
"available": list(self.REGISTRY.keys()) + ["reasoning_llm"],
|
| 835 |
"load_errors": self.load_errors,
|
| 836 |
+
"reasoning_llm": {
|
| 837 |
+
"last_engine": self.last_engine,
|
| 838 |
+
"local": local_llm.status(),
|
| 839 |
+
"hosted": {
|
| 840 |
+
"model": self.REASONING_LLM_REPO,
|
| 841 |
+
"usable": self.provider_available(),
|
| 842 |
+
},
|
| 843 |
+
},
|
| 844 |
}
|
| 845 |
|
| 846 |
@staticmethod
|
|
|
|
| 1127 |
"status": "healthy",
|
| 1128 |
"timestamp": datetime.utcnow().isoformat(),
|
| 1129 |
"services": {
|
| 1130 |
+
"reasoning_llm": transformer_loader.last_engine, # "hosted" | "local" | None until first answer
|
| 1131 |
+
"reasoning_model": transformer_loader.REASONING_LLM_REPO if HF_TOKEN else None,
|
| 1132 |
+
"local_llm": local_llm.status(),
|
| 1133 |
"ml_models": ml_loader.ready,
|
| 1134 |
"models_loaded": list(ml_loader.models.keys()),
|
| 1135 |
"transformers": transformer_loader.transformers_available,
|
|
|
|
| 1155 |
query = body.get("query", "")
|
| 1156 |
context = body.get("context", {})
|
| 1157 |
history = body.get("conversation_history", [])
|
| 1158 |
+
result = await run_in_threadpool(reasoning_service.analyze, query, context, history)
|
| 1159 |
return result
|
| 1160 |
|
| 1161 |
|
|
|
|
| 1233 |
"""Threat scanning – called by backend mlService.scanForThreats()"""
|
| 1234 |
body = await request.json()
|
| 1235 |
query = json.dumps(body.get("data", body), indent=1)[:3000]
|
| 1236 |
+
result = await run_in_threadpool(reasoning_service.analyze, f"Scan these indicators for threats:\n{query}")
|
| 1237 |
return result
|
| 1238 |
|
| 1239 |
|
|
|
|
| 1243 |
body = await request.json()
|
| 1244 |
query = body.get("query", "")
|
| 1245 |
context = body.get("context", {})
|
| 1246 |
+
result = await run_in_threadpool(reasoning_service.analyze, query, context)
|
| 1247 |
return {
|
| 1248 |
"insights": result.get("response", ""),
|
| 1249 |
"confidence": result.get("confidence", 0),
|
|
|
|
| 1302 |
|
| 1303 |
@api.post("/api/v2/security-chat")
|
| 1304 |
async def api_v2_security_chat(request: Request):
|
| 1305 |
+
"""Cybersecurity Q&A: hosted model when the account has credit, otherwise
|
| 1306 |
+
the open-source model running in this Space.
|
| 1307 |
Body: { "query": "...", "max_tokens": 512 }"""
|
| 1308 |
body = await request.json()
|
| 1309 |
query = body.get("query", "").strip()
|
| 1310 |
if not query:
|
| 1311 |
return JSONResponse(status_code=400, content={"detail": "query required"})
|
| 1312 |
max_tokens = int(body.get("max_tokens", 512))
|
| 1313 |
+
result = await run_in_threadpool(transformer_loader.security_chat, query, max_tokens)
|
| 1314 |
+
# Last resort: ML heuristic if no LLM answered
|
| 1315 |
+
if result.get("error"):
|
| 1316 |
+
return reasoning_service._fallback(query)
|
|
|
|
|
|
|
|
|
|
| 1317 |
return result
|
| 1318 |
|
| 1319 |
|
|
|
|
| 1424 |
"domain": "example.com", # optional
|
| 1425 |
"ip": "1.2.3.4", # optional
|
| 1426 |
"hash": "abc123...", # optional (future use)
|
| 1427 |
+
"context": { ... } # optional — passed to the LLM narrative
|
| 1428 |
}
|
| 1429 |
|
| 1430 |
Response: per-indicator results with unified risk summary.
|
|
|
|
| 1482 |
else:
|
| 1483 |
risk_level = "unknown"
|
| 1484 |
|
| 1485 |
+
# Optional LLM narrative. Hosted model only: the in-Space model takes many
|
| 1486 |
+
# seconds on CPU and this endpoint is called with short timeouts.
|
| 1487 |
ai_analysis = None
|
| 1488 |
+
if transformer_loader.provider_available() and (url or domain):
|
| 1489 |
target = url or domain
|
| 1490 |
+
ai_analysis = await run_in_threadpool(
|
| 1491 |
+
reasoning_service.analyze,
|
| 1492 |
f"Scan these IOC indicators for threats: url={target}, domain={effective_domain}",
|
| 1493 |
+
context, None, False,
|
| 1494 |
)
|
| 1495 |
|
| 1496 |
return {
|
|
|
|
| 1608 |
persistent vector store, which requires paid GPU on free tier).
|
| 1609 |
2. Translates raw ML signals / IOCs into plain human language when the
|
| 1610 |
user includes raw data and asks for an explanation.
|
| 1611 |
+
3. Uses the hosted model when the account has credit, otherwise the
|
| 1612 |
+
open-source model running in this Space,
|
| 1613 |
+
or the ML-powered heuristic analyzer when no LLM is available.
|
| 1614 |
|
| 1615 |
NOTE ON REAL-TIME "LEARNING": HuggingFace Spaces cpu-basic does NOT
|
| 1616 |
support fine-tuning or persistent model updates at runtime. What this
|
|
|
|
| 1777 |
n_models = len(ml_loader.models)
|
| 1778 |
sources.append({"type": "ml_models", "label": "ML Models", "value": f"{n_models}/4 loaded"})
|
| 1779 |
|
| 1780 |
+
# LLM cascade: hosted model (when the account has credit), then the
|
| 1781 |
+
# open-source model in this Space, then the ML heuristic below.
|
| 1782 |
+
llm_result = await run_in_threadpool(transformer_loader.security_chat, enriched_message, 600)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1783 |
if llm_result.get("response") and not llm_result.get("error"):
|
| 1784 |
text = llm_result["response"]
|
| 1785 |
text_lower = text.lower()
|
|
|
|
| 1802 |
"confidence": 0.72,
|
| 1803 |
"risk_level": risk_level,
|
| 1804 |
"risk_score": risk_score,
|
| 1805 |
+
"model_used": llm_result.get("model_id", LOCAL_LLM_REPO),
|
| 1806 |
"session_id": session_id,
|
| 1807 |
"sources": sources,
|
| 1808 |
"timestamp": datetime.utcnow().isoformat(),
|
| 1809 |
}
|
| 1810 |
|
| 1811 |
# Path 3: ML heuristic fallback (always available)
|
| 1812 |
+
fallback = reasoning_service._fallback(enriched_message)
|
| 1813 |
# Override with ML scan risk if available
|
| 1814 |
if detected_risk_score > 0:
|
| 1815 |
ml_score_100 = round(detected_risk_score * 100, 1)
|
|
|
|
| 1826 |
"""Network traffic analysis – called by backend mlService.analyzeNetworkTraffic()"""
|
| 1827 |
body = await request.json()
|
| 1828 |
traffic_data = body.get("traffic_data", {})
|
| 1829 |
+
result = await run_in_threadpool(
|
| 1830 |
+
reasoning_service.analyze,
|
| 1831 |
+
f"Analyze this network traffic for security threats:\n{json.dumps(traffic_data, indent=1)[:3000]}",
|
| 1832 |
)
|
| 1833 |
return result
|
| 1834 |
|
|
|
|
| 1839 |
body = await request.json()
|
| 1840 |
task_type = body.get("task_type", "")
|
| 1841 |
task_data = body.get("task_data", {})
|
| 1842 |
+
result = await run_in_threadpool(
|
| 1843 |
+
reasoning_service.analyze,
|
| 1844 |
+
f"Execute cybersecurity task '{task_type}':\n{json.dumps(task_data, indent=1)[:3000]}",
|
| 1845 |
)
|
| 1846 |
return result
|
| 1847 |
|
|
|
|
| 1851 |
"""Browser session analysis"""
|
| 1852 |
body = await request.json()
|
| 1853 |
session = body.get("session_data", body)
|
| 1854 |
+
result = await run_in_threadpool(
|
| 1855 |
+
reasoning_service.analyze,
|
| 1856 |
+
f"Analyze this browser session for security threats:\n{json.dumps(session, indent=1)[:3000]}",
|
| 1857 |
)
|
| 1858 |
return result
|
| 1859 |
|
|
|
|
| 1862 |
async def api_analyze_threat_feed(request: Request):
|
| 1863 |
"""Threat feed analysis"""
|
| 1864 |
body = await request.json()
|
| 1865 |
+
result = await run_in_threadpool(
|
| 1866 |
+
reasoning_service.analyze,
|
| 1867 |
+
f"Analyze this threat feed:\n{json.dumps(body, indent=1)[:3000]}",
|
| 1868 |
)
|
| 1869 |
return result
|
| 1870 |
|
|
|
|
| 1965 |
| Method | Endpoint | Description |
|
| 1966 |
|--------|----------|-------------|
|
| 1967 |
| GET | `/health` | Health check — version, models, services |
|
| 1968 |
+
| POST | `/analyze` | AI chat / general analysis (LLM or ML fallback) |
|
| 1969 |
| POST | `/analyze-url` | URL threat analysis — 4 ML models + risk factors |
|
| 1970 |
+
| POST | `/scan-threats` | Deep threat scan (LLM or ML fallback) |
|
| 1971 |
| POST | `/api/insights/generate` | AI-generated security insights |
|
| 1972 |
| POST | `/api/models/predict` | Direct ML model prediction |
|
| 1973 |
| GET | `/models` | List loaded models with source info |
|
|
|
|
| 1983 |
| POST | `/api/v2/ioc-scan` | Multi-IOC scanner: URL + domain + DGA + BERT |
|
| 1984 |
| POST | `/api/v2/url-enrich` | Enriched analysis: ML + BERT + DGA + explainability |
|
| 1985 |
| POST | `/api/v2/chat` | Security chatbot — context-grounded, RAG-lite, machine→human translation |
|
| 1986 |
+
| POST | `/api/v2/security-chat` | Security LLM Q&A (hosted model, or the open-source model in this Space) |
|
| 1987 |
| GET | `/api/v2/status` | Transformer model load status |
|
| 1988 |
|
| 1989 |
### Quick Examples
|
|
|
|
| 2025 |
|
| 2026 |
# Initialize services at startup
|
| 2027 |
logger.info("🚀 Initializing CyberForge AI services...")
|
|
|
|
| 2028 |
ml_loader.initialize()
|
| 2029 |
+
# Load the chat model in the background so the API answers health checks at once
|
| 2030 |
+
threading.Thread(target=local_llm.load, name="local-llm-loader", daemon=True).start()
|
| 2031 |
logger.info("✅ Services initialized")
|
| 2032 |
|
| 2033 |
# Create Gradio app and mount it on FastAPI
|
requirements.txt
CHANGED
|
@@ -19,14 +19,20 @@ gradio>=5.20.0
|
|
| 19 |
fastapi>=0.100.0
|
| 20 |
uvicorn[standard]>=0.22.0
|
| 21 |
|
| 22 |
-
# HF Hub
|
| 23 |
-
huggingface_hub>=0.
|
| 24 |
|
| 25 |
# Feature Engineering
|
| 26 |
tldextract>=5.1.0
|
| 27 |
|
| 28 |
-
#
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
# Notebook execution
|
| 32 |
nbformat>=5.10.0
|
|
|
|
| 19 |
fastapi>=0.100.0
|
| 20 |
uvicorn[standard]>=0.22.0
|
| 21 |
|
| 22 |
+
# HF Hub (>=0.26 for Inference Providers / OpenAI-compatible router used by DeepSeek)
|
| 23 |
+
huggingface_hub>=0.26.0
|
| 24 |
|
| 25 |
# Feature Engineering
|
| 26 |
tldextract>=5.1.0
|
| 27 |
|
| 28 |
+
# Reasoning LLM
|
| 29 |
+
# hosted: HF Inference Providers (OpenAI-compatible router), plain HTTPS via requests.
|
| 30 |
+
# local: open-source GGUF chat model run in-process with llama.cpp. The prebuilt
|
| 31 |
+
# CPU wheel is used so the image needs no compiler; --only-binary makes the
|
| 32 |
+
# build fail fast instead of compiling from source if that wheel is missing.
|
| 33 |
+
--extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cpu
|
| 34 |
+
--only-binary llama-cpp-python
|
| 35 |
+
llama-cpp-python==0.3.36
|
| 36 |
|
| 37 |
# Notebook execution
|
| 38 |
nbformat>=5.10.0
|