Spaces:
Sleeping
Sleeping
Update the CyberForge ML service
Browse files
app.py
CHANGED
|
@@ -16,6 +16,7 @@ import subprocess
|
|
| 16 |
import sys
|
| 17 |
import threading
|
| 18 |
import time
|
|
|
|
| 19 |
from pathlib import Path
|
| 20 |
from datetime import datetime
|
| 21 |
import logging
|
|
@@ -1318,12 +1319,77 @@ async def api_analyze(request: Request):
|
|
| 1318 |
return as_reasoning_text(result)
|
| 1319 |
|
| 1320 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1321 |
@api.post("/analyze-url")
|
| 1322 |
async def api_analyze_url(request: Request):
|
| 1323 |
"""URL analysis – called by backend mlService.analyzeWebsite()
|
| 1324 |
Returns per-model predictions + unified aggregate with risk_score (0-100).
|
| 1325 |
All models now consistently include threat_score (probability of class-1).
|
| 1326 |
"""
|
|
|
|
| 1327 |
body = await json_object(request)
|
| 1328 |
url = text_field(body, "url")
|
| 1329 |
if not url:
|
|
@@ -1384,6 +1450,7 @@ async def api_analyze_url(request: Request):
|
|
| 1384 |
"model_predictions": predictions,
|
| 1385 |
"features_analyzed": features,
|
| 1386 |
"timestamp": datetime.utcnow().isoformat(),
|
|
|
|
| 1387 |
}
|
| 1388 |
|
| 1389 |
|
|
|
|
| 16 |
import sys
|
| 17 |
import threading
|
| 18 |
import time
|
| 19 |
+
import uuid
|
| 20 |
from pathlib import Path
|
| 21 |
from datetime import datetime
|
| 22 |
import logging
|
|
|
|
| 1319 |
return as_reasoning_text(result)
|
| 1320 |
|
| 1321 |
|
| 1322 |
+
ANALYSIS_SCHEMA = "cyberforge.ml.analysis.v1"
|
| 1323 |
+
VERDICT_POLICY = "trained_models_v1"
|
| 1324 |
+
|
| 1325 |
+
|
| 1326 |
+
def analysis_record(target: str, predictions: Dict, started: float) -> Dict:
|
| 1327 |
+
"""The versioned record of one analysis (V2-ML02), beside the older fields.
|
| 1328 |
+
|
| 1329 |
+
Verdict policy trained_models_v1: "threat" when any trained model answered
|
| 1330 |
+
and flagged the target, "benign" when trained models answered and none
|
| 1331 |
+
flagged it, "unknown" when no trained model answered (rule-based estimates
|
| 1332 |
+
only). Scores are each model's output rescaled at its decision threshold:
|
| 1333 |
+
raw scores, not calibrated probabilities, so no probability is given."""
|
| 1334 |
+
detectors, warnings = [], []
|
| 1335 |
+
for name, p in predictions.items():
|
| 1336 |
+
trained = p.get("inference_source") == "ml_model"
|
| 1337 |
+
meta = ml_loader.metadata.get(name, {}) if trained else {}
|
| 1338 |
+
if "error" in p:
|
| 1339 |
+
verdict = "unknown"
|
| 1340 |
+
else:
|
| 1341 |
+
verdict = "threat" if p.get("prediction") == 1 else "benign"
|
| 1342 |
+
detectors.append({
|
| 1343 |
+
"id": name,
|
| 1344 |
+
"engine": "trained_model" if trained else "heuristic",
|
| 1345 |
+
"verdict": verdict,
|
| 1346 |
+
"raw_score": p.get("threat_score"),
|
| 1347 |
+
"calibrated_probability": None,
|
| 1348 |
+
"trained_at": meta.get("trained_at"),
|
| 1349 |
+
"decision_threshold": meta.get("decision_threshold"),
|
| 1350 |
+
"feature_set": meta.get("feature_set"),
|
| 1351 |
+
"missing_features": p.get("missing_features"),
|
| 1352 |
+
})
|
| 1353 |
+
if not trained:
|
| 1354 |
+
why = p.get("note") or ("features were missing" if p.get("missing_features") else "the model is not loaded")
|
| 1355 |
+
warnings.append(f"{name}: rule-based estimate, not the trained model ({why})")
|
| 1356 |
+
answered = [d for d in detectors if d["engine"] == "trained_model" and d["verdict"] != "unknown"]
|
| 1357 |
+
if not answered:
|
| 1358 |
+
verdict, reason = "unknown", "no trained model answered; only rule-based estimates are available"
|
| 1359 |
+
elif any(d["verdict"] == "threat" for d in answered):
|
| 1360 |
+
flagged = [d["id"] for d in answered if d["verdict"] == "threat"]
|
| 1361 |
+
verdict, reason = "threat", f"flagged by {', '.join(flagged)}"
|
| 1362 |
+
else:
|
| 1363 |
+
verdict, reason = "benign", f"{len(answered)} trained model(s) answered and none flagged it"
|
| 1364 |
+
warnings.append("scores are model outputs rescaled at each decision threshold, not calibrated probabilities")
|
| 1365 |
+
return {
|
| 1366 |
+
"schema": ANALYSIS_SCHEMA,
|
| 1367 |
+
"analysis_id": uuid.uuid4().hex,
|
| 1368 |
+
"task": "url",
|
| 1369 |
+
"target": target,
|
| 1370 |
+
"verdict": verdict,
|
| 1371 |
+
"verdict_reason": reason,
|
| 1372 |
+
"policy": VERDICT_POLICY,
|
| 1373 |
+
"coverage": {
|
| 1374 |
+
"detectors": len(detectors),
|
| 1375 |
+
"trained_models_answered": len(answered),
|
| 1376 |
+
"heuristic_estimates": sum(1 for d in detectors if d["engine"] == "heuristic"),
|
| 1377 |
+
"reputation_list_loaded": popular_domains_loaded(),
|
| 1378 |
+
},
|
| 1379 |
+
"detectors": detectors,
|
| 1380 |
+
"service_version": SERVICE_VERSION,
|
| 1381 |
+
"duration_ms": round((time.perf_counter() - started) * 1000, 1),
|
| 1382 |
+
"warnings": warnings,
|
| 1383 |
+
}
|
| 1384 |
+
|
| 1385 |
+
|
| 1386 |
@api.post("/analyze-url")
|
| 1387 |
async def api_analyze_url(request: Request):
|
| 1388 |
"""URL analysis – called by backend mlService.analyzeWebsite()
|
| 1389 |
Returns per-model predictions + unified aggregate with risk_score (0-100).
|
| 1390 |
All models now consistently include threat_score (probability of class-1).
|
| 1391 |
"""
|
| 1392 |
+
started = time.perf_counter()
|
| 1393 |
body = await json_object(request)
|
| 1394 |
url = text_field(body, "url")
|
| 1395 |
if not url:
|
|
|
|
| 1450 |
"model_predictions": predictions,
|
| 1451 |
"features_analyzed": features,
|
| 1452 |
"timestamp": datetime.utcnow().isoformat(),
|
| 1453 |
+
"analysis": analysis_record(url, predictions, started),
|
| 1454 |
}
|
| 1455 |
|
| 1456 |
|