Spaces:
Sleeping
Sleeping
Che237 commited on
Commit ·
91af9cd
0
Parent(s):
Wave 2: Context-grounded security chatbot — /api/v2/chat upgrade
Browse files- Dockerfile +38 -0
- README.md +45 -0
- app.py +1945 -0
- 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
|