Authentication and Odata compliance
Build, push image, and notify Watchtower / build-image (push) Successful in 39s
Build, push image, and notify Watchtower / notify (push) Successful in 13s

This commit is contained in:
2026-08-18 17:04:02 +02:00
parent e69bbc6b45
commit f9309c4dbc
2 changed files with 357 additions and 185 deletions
+271 -140
View File
@@ -1,12 +1,16 @@
import os import os
import asyncio
import base64
import uuid
from datetime import datetime, timedelta, timezone
import sys import sys
import json import json
from flask import Flask, request, jsonify, current_app import uuid
from pymongo import MongoClient import base64
import asyncio
import fcntl
import atexit
from datetime import datetime, timedelta, timezone
from flask import Flask, Response, request, jsonify, current_app, abort, g
from pymongo import MongoClient, ASCENDING, DESCENDING
from apscheduler.schedulers.background import BackgroundScheduler
from APIs import generate from APIs import generate
from APIs.mqtt import send_command from APIs.mqtt import send_command
from APIs.webex import WebexManager from APIs.webex import WebexManager
@@ -14,8 +18,6 @@ from APIs.shodan import ShodanAuditor
from APIs.twilio import send_alert_sms from APIs.twilio import send_alert_sms
from microwaveCookPlanner import MicrowaveCookPlanner from microwaveCookPlanner import MicrowaveCookPlanner
import safety_checker import safety_checker
import pymongo
from apscheduler.schedulers.background import BackgroundScheduler
from jobs import job_server_cve_audit, job_client_ip_audit, job_request_telemetry from jobs import job_server_cve_audit, job_client_ip_audit, job_request_telemetry
sys.path.insert(0, '..') sys.path.insert(0, '..')
@@ -35,16 +37,25 @@ MONGO_URI = os.getenv("MONGO_URI", "mongodb://localhost:27017/")
client = MongoClient(MONGO_URI) client = MongoClient(MONGO_URI)
db = client["microwave_network_db"] db = client["microwave_network_db"]
# Database Collections
client_collection = db["client_data"] client_collection = db["client_data"]
cooking_collection = db["cooking_parameters"] cooking_collection = db["cooking_parameters"]
telemetry_collection = db["telemetry_data"] telemetry_collection = db["telemetry_data"]
alert_collection = db["alert_data"] alert_collection = db["alert_data"]
webex_tokens_collection = db["webex_tokens"] # Collection for Webex OAuth tokens webex_tokens_collection = db["webex_tokens"]
device_ip_collection = db["device_ips"] # Collection for device IPs device_ip_collection = db["device_ips"]
cve_audit_results = db["cve_audit_results"] # Collection for CVE audit results cve_audit_results = db["cve_audit_results"]
# Initialize Database Indexes
def init_db_indexes():
device_ip_collection.create_index([("received_at", DESCENDING)])
device_ip_collection.create_index([("client_ip", ASCENDING)])
alert_collection.create_index([("orchestrator_id", ASCENDING), ("created_at", DESCENDING)])
client_collection.create_index([("devices.device_id", ASCENDING)])
# Webex Credentials & Configuration from Environment Variables init_db_indexes()
# Environment Configurations
WEBEX_CLIENT_ID = os.getenv("WEBEX_CLIENT_ID", "YOUR_WEBEX_CLIENT_ID") WEBEX_CLIENT_ID = os.getenv("WEBEX_CLIENT_ID", "YOUR_WEBEX_CLIENT_ID")
WEBEX_CLIENT_SECRET = os.getenv("WEBEX_CLIENT_SECRET", "YOUR_WEBEX_CLIENT_SECRET") WEBEX_CLIENT_SECRET = os.getenv("WEBEX_CLIENT_SECRET", "YOUR_WEBEX_CLIENT_SECRET")
WEBEX_REDIRECT_URI = os.getenv("WEBEX_REDIRECT_URI", "https://smartwave.matthiasg.dev/oauth/callback") WEBEX_REDIRECT_URI = os.getenv("WEBEX_REDIRECT_URI", "https://smartwave.matthiasg.dev/oauth/callback")
@@ -57,10 +68,9 @@ CAMERA_IMAGE_DIR = "storage/dishPhotos"
os.makedirs(CAMERA_IMAGE_DIR, exist_ok=True) os.makedirs(CAMERA_IMAGE_DIR, exist_ok=True)
# --------------------------------------------------------- # ---------------------------------------------------------
# Classes # Integrations Setup
# --------------------------------------------------------- # ---------------------------------------------------------
microwave_cook_planner = MicrowaveCookPlanner() microwave_cook_planner = MicrowaveCookPlanner()
# Instantiate Webex Manager
webex_manager = WebexManager( webex_manager = WebexManager(
db_collection=webex_tokens_collection, db_collection=webex_tokens_collection,
client_id=WEBEX_CLIENT_ID, client_id=WEBEX_CLIENT_ID,
@@ -71,60 +81,110 @@ webex_manager = WebexManager(
) )
shodan_auditor = ShodanAuditor() shodan_auditor = ShodanAuditor()
# ------------------------------------------------------------- # ---------------------------------------------------------
# Cron Scheduler Setup # Cron Scheduler (Multi-Worker Safe)
# ------------------------------------------------------------- # ---------------------------------------------------------
scheduler = BackgroundScheduler(daemon=True) scheduler = BackgroundScheduler(daemon=True)
# Job 1: Server CVE audit every night at 00:00 UTC def start_scheduler_once():
scheduler.add_job( """Ensures only ONE Gunicorn worker process runs the cron scheduler."""
lock_file_path = "/tmp/scheduler.lock"
# Open or create a lock file
lock_file = open(lock_file_path, "wb")
try:
# Request a non-blocking exclusive lock
fcntl.flock(lock_file, fcntl.LOCK_EX | fcntl.LOCK_NB)
# Register job schedules
scheduler.add_job(
func=job_server_cve_audit, func=job_server_cve_audit,
args=[shodan_auditor, db], args=[shodan_auditor, db],
trigger="cron", trigger="cron",
hour=0, hour=0,
minute=0, minute=0,
id="server_cve_audit_job" id="server_cve_audit_job",
) replace_existing=True
)
# Job 2: Client IP Shodan audit every 6 hours scheduler.add_job(
scheduler.add_job(
func=job_client_ip_audit, func=job_client_ip_audit,
args=[shodan_auditor, db], args=[shodan_auditor, db],
trigger="cron", trigger="cron",
hour="*/6", hour="*/6",
minute=15, minute=15,
id="client_ip_audit_job" id="client_ip_audit_job",
) replace_existing=True
)
# Job 3: Request telemetry runs AFTER the client audit (e.g., every 12 hours) scheduler.add_job(
scheduler.add_job(
func=job_request_telemetry, func=job_request_telemetry,
args=[send_command], args=[send_command],
trigger="cron", trigger="cron",
hour="1,13", hour="1,13",
minute=0, minute=0,
id="request_telemetry_job" id="request_telemetry_job",
) replace_existing=True
)
scheduler.start() scheduler.start()
print(f"[Cron] Scheduler started successfully in Worker PID {os.getpid()}")
# ---------------------------------------------------------
# OAuth Routes
# ---------------------------------------------------------
@app.route("/oauth/callback")
def oauth_callback():
"""OAuth redirect endpoint that receives the authorization code."""
code = request.args.get("code")
if not code:
return jsonify({"error": "Missing code parameter"}), 400
# Ensure lock release on shutdown
def cleanup():
try: try:
webex_manager.exchange_code(code) fcntl.flock(lock_file, fcntl.LOCK_UN)
return jsonify({"status": "success", "message": "Webex tokens stored successfully in MongoDB!"}), 200 lock_file.close()
except Exception as e: except Exception:
current_app.logger.exception("Failed to exchange OAuth code") pass
return jsonify({"error": f"OAuth exchange failed: {str(e)}"}), 500
atexit.register(cleanup)
except (IOError, OSError):
# Lock acquired by another worker - skip starting scheduler
print(f"[Cron] Worker PID {os.getpid()} skipped scheduler (already running in another worker).")
# Initialize single-instance scheduler
start_scheduler_once()
# ---------------------------------------------------------
# Authentication Middleware
# ---------------------------------------------------------
EXEMPT_ROUTES = {'hello_world', 'oauth_callback', 'odata_metadata'}
@app.before_request
def authenticate_request():
if request.endpoint in EXEMPT_ROUTES or request.method == 'OPTIONS':
return
# Extract API Key from Header (X-API-Key or Bearer token)
api_key = request.headers.get("X-API-Key")
if not api_key:
auth_header = request.headers.get("Authorization", "")
if auth_header.startswith("Bearer "):
api_key = auth_header.split(" ")[1]
if not api_key:
return jsonify({
"@odata.error": {
"code": "401",
"message": "Unauthorized: Missing API Key header"
}
}), 401
# Validate key against 'devices_authentication' collection
device_auth = db.devices_authentication.find_one({"api_key": api_key})
if not device_auth:
return jsonify({
"@odata.error": {
"code": "401",
"message": "Unauthorized: Invalid API Key"
}
}), 401
# Attach device context (works for both Microwaves and Node-RED)
g.device_id = device_auth.get("device_id")
# --------------------------------------------------------- # ---------------------------------------------------------
# Application Routes # Application Routes
@@ -133,56 +193,58 @@ def oauth_callback():
@app.route("/") @app.route("/")
def hello_world(): def hello_world():
gen = generate(prompt="Say Hello, to the user !") gen = generate(prompt="Say Hello, to the user !")
print(gen)
return f"<p>{gen}</p>" return f"<p>{gen}</p>"
@app.route("/oauth/callback")
def oauth_callback():
code = request.args.get("code")
if not code:
return jsonify({"error": "Missing code parameter"}), 400
try:
webex_manager.exchange_code(code)
return jsonify({"status": "success", "message": "Webex tokens stored successfully!"}), 200
except Exception as e:
current_app.logger.exception("Failed to exchange OAuth code")
return jsonify({"error": "OAuth exchange failed"}), 500
@app.route("/cooking-params", methods=["POST"]) @app.route("/cooking-params", methods=["POST"])
async def cooking_params(): async def cooking_params():
try: try:
data = request.get_json() data = request.get_json()
microwave_id = data.get("microwave_id", "unknown") if not data or not isinstance(data, dict):
return jsonify({"@odata.error": {"code": "400", "message": "Invalid payload"}}), 400
data["microwave_id"] = g.device_id
forwarded_for = request.headers.get('X-Forwarded-For')
client_ip = forwarded_for.split(',')[0].strip() if forwarded_for else request.remote_addr
client_ip = request.headers.get('X-Forwarded-For', request.remote_addr).split(',')[0].strip()
device_ip_collection.insert_one({ device_ip_collection.insert_one({
"microwave_id": microwave_id, "microwave_id": g.device_id,
"client_ip": client_ip, "client_ip": client_ip,
"received_at": datetime.now(timezone.utc) "received_at": datetime.now(timezone.utc)
}) })
if not data:
return jsonify({"error": "Invalid or missing JSON payload"}), 400
# Extract user or device parameters
height_cm = float(data.get("dish_height", 4.0)) height_cm = float(data.get("dish_height", 4.0))
initial_temp_c = float(data.get("ir_initial_temp", 20.0)) initial_temp_c = float(data.get("ir_initial_temp", 20.0))
microwave_wattage = int(data.get("microwave_wattage", 900)) microwave_wattage = int(data.get("microwave_wattage", 900))
defrost_mode = bool(data.get("defrost_mode", False)) defrost_mode = bool(data.get("defrost_mode", False))
# 1. Save Camera Image
camera_image_b64 = data.get("camera_image") camera_image_b64 = data.get("camera_image")
if not camera_image_b64: if not camera_image_b64:
return jsonify({"error": "Missing required field 'camera_image'"}), 400 return jsonify({"@odata.error": {"code": "400", "message": "Missing camera_image"}}), 400
filename = f"dish_{uuid.uuid4().hex}.jpg" filename = f"dish_{uuid.uuid4().hex}.jpg"
filepath = os.path.join(CAMERA_IMAGE_DIR, filename) filepath = os.path.join(CAMERA_IMAGE_DIR, filename)
try:
with open(filepath, "wb") as f: with open(filepath, "wb") as f:
f.write(base64.b64decode(camera_image_b64)) f.write(base64.b64decode(camera_image_b64))
data["camera_image"] = filepath data["camera_image"] = filepath
except Exception as e:
current_app.logger.exception("Failed to save camera image")
return jsonify({"error": f"Failed to save camera image: {str(e)}"}), 500
# 2. Run Vision Safety Check & Cook Planner Concurrently # Concurrent AI Execution
try:
if getattr(config, "DEBUG_DANGEROUS_AREA", False):
print(f"[Debug] Running safety check on predefined image")
safety_task = asyncio.to_thread(safety_checker.check_dish_safety, os.path.join(CAMERA_IMAGE_DIR, "dish_0de0ee1dab8949fc8e78796947e24ed3.jpg"))
elif getattr(config, "DEBUG", False):
safety_task = asyncio.to_thread(safety_checker.check_dish_safety, os.path.join("microwaveDish.jpg"))
else:
safety_task = asyncio.to_thread(safety_checker.check_dish_safety, filepath) safety_task = asyncio.to_thread(safety_checker.check_dish_safety, filepath)
planner_task = asyncio.to_thread( planner_task = asyncio.to_thread(
microwave_cook_planner.generate_plan, microwave_cook_planner.generate_plan,
@@ -193,54 +255,68 @@ async def cooking_params():
defrost_mode=defrost_mode defrost_mode=defrost_mode
) )
# Execute both concurrently and await results
safety_result, cook_plan = await asyncio.gather(safety_task, planner_task) safety_result, cook_plan = await asyncio.gather(safety_task, planner_task)
except Exception as e:
current_app.logger.exception("Exception during safety check or cook planning")
return jsonify({"error": f"Task execution failed: {str(e)}"}), 500
# 4. Attach Cooking Plan & safety check and Save to MongoDB
data["safety_check"] = safety_result data["safety_check"] = safety_result
data["analysis_results"] = cook_plan data["analysis_results"] = cook_plan
try: inserted = cooking_collection.insert_one(data)
cooking_collection.insert_one(data) doc_id = str(inserted.inserted_id)
data.pop("_id", None)
except Exception as e: # Base OData v4 response wrapper
return jsonify({"error": f"Database error: {str(e)}"}), 500 odata_response = {
"@odata.context": f"{request.host_url.rstrip('/')}/$metadata#CookingParams/$entity",
"@odata.id": f"{request.host_url.rstrip('/')}/cooking-params('{doc_id}')",
"id": doc_id,
"plan": cook_plan
}
# 3. Evaluate Safety Result
if not safety_result.get("is_safe", True): if not safety_result.get("is_safe", True):
current_app.logger.warning(f"Unsafe dish detected: {safety_result}") odata_response.update({
return jsonify({
"error": "Safety hazard detected in microwave area",
"is_safe": False, "is_safe": False,
"warning_message": safety_result.get("warning_message", "Unsafe materials detected."), "warning_message": safety_result.get("warning_message", "Unsafe materials detected."),
"detected_hazards": safety_result.get("detected_hazards", []) "detected_hazards": safety_result.get("detected_hazards", [])
}), 200 })
return jsonify(odata_response), 200
return jsonify(odata_response), 201
return jsonify(cook_plan), 201
except Exception as e: except Exception as e:
current_app.logger.exception("Exception in /cooking-params") current_app.logger.exception("Error in /cooking-params")
return jsonify({"@odata.error": {"code": "500", "message": "Internal processing error"}}), 500
@app.route("/alert", methods=["POST"]) @app.route("/alert", methods=["POST"])
def alert(): def alert():
data = request.get_json() or {} data = request.get_json()
if data is None or not isinstance(data, dict):
return jsonify({
"@odata.error": {
"code": "400",
"message": "Invalid or missing JSON payload"
}
}), 400
# 1. Inject UTC timestamp # Auto-populate or override orchestrator_id using authenticated device context
orchestrator_id = data.get("orchestrator_id") or getattr(g, "device_id", None)
data["orchestrator_id"] = orchestrator_id
data["created_at"] = datetime.now(timezone.utc) data["created_at"] = datetime.now(timezone.utc)
saved_alert = alert_collection.insert_one(data)
orchestrator_id = data.get("orchestrator_id") try:
saved_alert = alert_collection.insert_one(data)
doc_id = str(saved_alert.inserted_id)
except Exception as e:
current_app.logger.exception("Failed to insert alert into database")
return jsonify({
"@odata.error": {
"code": "500",
"message": "Database write failed"
}
}), 500
alert_info = data.get("alert", {}) alert_info = data.get("alert", {})
alert_type = alert_info.get("type", "Unknown") alert_type = alert_info.get("type", "Unknown")
alert_message = alert_info.get("message", "No message provided") alert_message = alert_info.get("message", "No message provided")
# --- GET CLIENT METADATA ---
client_name = f"Unknown Client (Orchestrator {orchestrator_id})" if orchestrator_id else "Unknown Client" client_name = f"Unknown Client (Orchestrator {orchestrator_id})" if orchestrator_id else "Unknown Client"
client_email = None client_email = None
client_phone = None client_phone = None
@@ -259,14 +335,12 @@ def alert():
client_name = client_doc.get("client_name", client_name) client_name = client_doc.get("client_name", client_name)
contact_info = client_doc.get("contact_info", {}) contact_info = client_doc.get("contact_info", {})
client_email = contact_info.get("email") client_email = contact_info.get("email")
client_phone = contact_info.get("phone") # Extract phone number for Twilio client_phone = contact_info.get("phone")
# --- WEBEX ROOM LOGIC ---
webex_room_id = None webex_room_id = None
room_title = f"Support - {client_name}" room_title = f"Support - {client_name}"
room_link = None room_link = None
webex_status = "skipped" webex_status = "skipped"
# --- TWILIO SMS LOGIC ---
sms_sent = False sms_sent = False
three_minutes_ago = datetime.now(timezone.utc) - timedelta(minutes=3) three_minutes_ago = datetime.now(timezone.utc) - timedelta(minutes=3)
@@ -276,7 +350,7 @@ def alert():
"webex_room_id": {"$exists": True, "$ne": None}, "webex_room_id": {"$exists": True, "$ne": None},
"created_at": {"$gte": three_minutes_ago}, "created_at": {"$gte": three_minutes_ago},
"_id": {"$ne": saved_alert.inserted_id} "_id": {"$ne": saved_alert.inserted_id}
}, sort=[("created_at", pymongo.DESCENDING)]) }, sort=[("created_at", DESCENDING)])
if recent_alert: if recent_alert:
webex_room_id = recent_alert["webex_room_id"] webex_room_id = recent_alert["webex_room_id"]
@@ -286,15 +360,14 @@ def alert():
room_id=webex_room_id, room_id=webex_room_id,
alert_type=alert_type, alert_type=alert_type,
alert_message=alert_message, alert_message=alert_message,
alert_id=str(saved_alert.inserted_id), alert_id=doc_id,
added_alert_info=True added_alert_info=True
) )
webex_status = "reused_room" webex_status = "reused_room"
room_link = f"https://web.webex.com/spaces/{webex_room_id}" room_link = recent_alert.get("webex_room_link") or f"https://web.webex.com/spaces/{webex_room_id}"
except Exception as e: except Exception as e:
current_app.logger.exception("Failed to send follow-up message to existing Webex room") current_app.logger.exception("Failed to send follow-up message to Webex room")
webex_status = f"message_failed: {str(e)}" webex_status = f"message_failed: {str(e)}"
else: else:
try: try:
room_details = webex_manager.create_support_room( room_details = webex_manager.create_support_room(
@@ -302,25 +375,22 @@ def alert():
client_email, client_email,
alert_type, alert_type,
alert_message, alert_message,
str(saved_alert.inserted_id) doc_id
) )
if isinstance(room_details, dict): if isinstance(room_details, dict):
webex_room_id = room_details.get("id") webex_room_id = room_details.get("id")
room_title = room_details.get("title", room_title) room_title = room_details.get("title", room_title)
# Set room_link if your webex_manager returns meetingLink or custom URL
room_link = room_details.get("meetingLink") or f"https://web.webex.com/spaces/{webex_room_id}" room_link = room_details.get("meetingLink") or f"https://web.webex.com/spaces/{webex_room_id}"
else: else:
webex_room_id = room_details webex_room_id = room_details
room_link = f"https://web.webex.com/spaces/{webex_room_id}" room_link = f"https://web.webex.com/spaces/{webex_room_id}"
webex_status = "created_new_room" webex_status = "created_new_room"
except Exception as e: except Exception as e:
current_app.logger.exception("Failed to create Webex support room") current_app.logger.exception("Failed to create Webex support room")
webex_status = f"creation_failed: {str(e)}" webex_status = f"creation_failed: {str(e)}"
# --- SEND SMS ---
if webex_room_id and client_phone and not SAVEUP_TWILIO_API_TOKEN: if webex_room_id and client_phone and not SAVEUP_TWILIO_API_TOKEN:
sms_sent = send_alert_sms( sms_sent = send_alert_sms(
to_phone=client_phone, to_phone=client_phone,
@@ -329,71 +399,91 @@ def alert():
client_email=client_email client_email=client_email
) )
# Update document with room and sms details update_fields = {"sms_sent": sms_sent}
if webex_room_id: if webex_room_id:
alert_collection.update_one( update_fields.update({
{"_id": saved_alert.inserted_id}, "webex_room_id": webex_room_id,
{"$set": {"webex_room_id": webex_room_id, "webex_room_title": room_title, "webex_room_link": room_link, "sms_sent": sms_sent}} "webex_room_title": room_title,
) "webex_room_link": room_link
else: # Just save the sms })
alert_collection.update_one(
{"_id": saved_alert.inserted_id},
{"$set": {"sms_sent": sms_sent}}
)
alert_collection.update_one({"_id": saved_alert.inserted_id}, {"$set": update_fields})
# OData v4 Created Response Payload
return jsonify({ return jsonify({
"@odata.context": f"{request.host_url.rstrip('/')}/$metadata#Alerts/$entity",
"@odata.id": f"{request.host_url.rstrip('/')}/alert('{doc_id}')",
"id": doc_id,
"status": "success", "status": "success",
"message": "Alert saved", "message": "Alert saved",
"orchestrator_id": orchestrator_id,
"webex_room_id": webex_room_id, "webex_room_id": webex_room_id,
"webex_room_url": room_link, "webex_room_url": room_link,
"webex_status": webex_status, "webex_status": webex_status,
"sms_sent": sms_sent "sms_sent": sms_sent
}), 200 }), 201
@app.route("/telemetry", methods=["POST"]) @app.route("/telemetry", methods=["POST"])
def telemetry(): def telemetry():
data = request.get_json() data = request.get_json()
if data is None: if data is None:
return jsonify({"error": "Invalid or missing JSON payload"}), 400 return jsonify({"@odata.error": {"code": "400", "message": "Invalid JSON payload"}}), 400
# If the payload was double-encoded as a string, deserialize it
if isinstance(data, str): if isinstance(data, str):
try: try:
data = json.loads(data) data = json.loads(data)
except (json.JSONDecodeError, TypeError): except (json.JSONDecodeError, TypeError):
return jsonify({"error": "String payload could not be parsed as JSON"}), 400 return jsonify({"@odata.error": {"code": "400", "message": "String payload invalid"}}), 400
if not isinstance(data, dict): data["device_id"] = g.device_id
return jsonify({"error": "Expected a JSON object/dictionary"}), 400
# Stamp UTC timestamp for Node-RED queries
data["received_at"] = datetime.now(timezone.utc).isoformat() data["received_at"] = datetime.now(timezone.utc).isoformat()
try: try:
telemetry_collection.insert_one(data) inserted = telemetry_collection.insert_one(data)
return jsonify({"status": "success", "message": "Telemetry saved"}), 200 doc_id = str(inserted.inserted_id)
# OData v4 Created Response Envelope
return jsonify({
"@odata.context": f"{request.host_url.rstrip('/')}/$metadata#Telemetry/$entity",
"@odata.id": f"{request.host_url.rstrip('/')}/telemetry('{doc_id}')",
"id": doc_id,
"status": "success",
"message": "Telemetry saved",
"device_id": g.device_id,
"received_at": data["received_at"]
}), 201
except Exception as e: except Exception as e:
return jsonify({"error": f"Database error: {str(e)}"}), 500 current_app.logger.exception("Database write failed")
return jsonify({"@odata.error": {"code": "500", "message": "Database write failed"}}), 500
# -------------------------------------------------------------
# Server CVE & SSL Audit Endpoint @app.route('/api/security/cve-audit', methods=['POST', 'GET'])
# -------------------------------------------------------------
@app.route('/api/security/cve-audit', methods=['GET'])
def run_cve_audit(): def run_cve_audit():
"""Runs a vulnerability and SSL audit against this server.""" """Protected endpoint triggered by Node-RED using REST API + API Key."""
audit_results = shodan_auditor.audit_server_vulnerabilities() audit_results = shodan_auditor.audit_server_vulnerabilities()
cve_audit_results.insert_one({ inserted = cve_audit_results.insert_one({
"triggered_by": g.device_id,
"audit_results": audit_results, "audit_results": audit_results,
"timestamp": datetime.now(timezone.utc) "timestamp": datetime.now(timezone.utc)
}) })
doc_id = str(inserted.inserted_id)
status_code = 200 if audit_results["status"] == "PASS" else 409 status_code = 200 if audit_results.get("status") == "PASS" else 409
return jsonify(audit_results), status_code
return jsonify({
"@odata.context": f"{request.host_url.rstrip('/')}/$metadata#CveAudit/$entity",
"@odata.id": f"{request.host_url.rstrip('/')}/api/security/cve-audit('{doc_id}')",
"id": doc_id,
"status": audit_results.get("status"),
"audit_results": audit_results,
"timestamp": datetime.now(timezone.utc).isoformat()
}), status_code
# ---------------------------------------------------------
# Debug Endpoints (Protected)
# ---------------------------------------------------------
@app.route("/debug/telemetryrequest", methods=["GET"]) @app.route("/debug/telemetryrequest", methods=["GET"])
def debug_telemetryrequest(): def debug_telemetryrequest():
@@ -414,6 +504,7 @@ def debug_telemetryrequest():
except Exception as e: except Exception as e:
return jsonify({"error": f"Failed to publish MQTT command: {str(e)}"}), 500 return jsonify({"error": f"Failed to publish MQTT command: {str(e)}"}), 500
@app.route('/debug/run-job/<job_id>', methods=['POST', 'GET']) @app.route('/debug/run-job/<job_id>', methods=['POST', 'GET'])
def trigger_job_manually(job_id): def trigger_job_manually(job_id):
"""Manually triggers any scheduled job immediately by its ID.""" """Manually triggers any scheduled job immediately by its ID."""
@@ -424,7 +515,6 @@ def trigger_job_manually(job_id):
"available_jobs": [j.id for j in scheduler.get_jobs()] "available_jobs": [j.id for j in scheduler.get_jobs()]
}), 404 }), 404
# Execute the underlying function with its configured arguments
try: try:
job.func(*job.args) job.func(*job.args)
return jsonify({ return jsonify({
@@ -437,9 +527,50 @@ def trigger_job_manually(job_id):
"message": f"Job execution failed: {str(e)}" "message": f"Job execution failed: {str(e)}"
}), 500 }), 500
@app.route("/debug", methods=["GET"]) # ---------------------------------------------------------
def debug(): # OData Metadata Definition
return safety_checker.check_dish_safety(os.path.join(CAMERA_IMAGE_DIR, "dish_0de0ee1dab8949fc8e78796947e24ed3.jpg")) # ---------------------------------------------------------
@app.route("/$metadata", methods=["GET"])
def odata_metadata():
"""Provides mandatory OData schema definition for validation."""
xml_metadata = """<?xml version="1.0" encoding="utf-8"?>
<edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx">
<edmx:DataServices>
<Schema Namespace="MicrowaveNetwork" xmlns="http://docs.oasis-open.org/odata/ns/edm">
<EntityType Name="Telemetry">
<Key><PropertyRef Name="id"/></Key>
<Property Name="id" Type="Edm.String" Nullable="false"/>
<Property Name="device_id" Type="Edm.String"/>
<Property Name="received_at" Type="Edm.DateTimeOffset"/>
</EntityType>
<EntityType Name="CookingParams">
<Key><PropertyRef Name="id"/></Key>
<Property Name="id" Type="Edm.String" Nullable="false"/>
<Property Name="microwave_id" Type="Edm.String"/>
</EntityType>
<EntityType Name="CveAudit">
<Key><PropertyRef Name="id"/></Key>
<Property Name="id" Type="Edm.String" Nullable="false"/>
<Property Name="status" Type="Edm.String"/>
</EntityType>
<EntityType Name="Alert">
<Key><PropertyRef Name="id"/></Key>
<Property Name="id" Type="Edm.String" Nullable="false"/>
<Property Name="orchestrator_id" Type="Edm.String"/>
<Property Name="webex_room_id" Type="Edm.String"/>
<Property Name="webex_status" Type="Edm.String"/>
<Property Name="sms_sent" Type="Edm.Boolean"/>
</EntityType>
<EntityContainer Name="Container">
<EntitySet Name="Telemetry" EntityType="MicrowaveNetwork.Telemetry"/>
<EntitySet Name="CookingParams" EntityType="MicrowaveNetwork.CookingParams"/>
<EntitySet Name="CveAudit" EntityType="MicrowaveNetwork.CveAudit"/>
<EntitySet Name="Alerts" EntityType="MicrowaveNetwork.Alert"/>
</EntityContainer>
</Schema>
</edmx:DataServices>
</edmx:Edmx>"""
return Response(xml_metadata, mimetype="application/xml")
if __name__ == "__main__": if __name__ == "__main__":
+41
View File
@@ -0,0 +1,41 @@
def parse_odata_query(request_args):
"""
Translates standard OData query parameters ($top, $skip, $orderby, $filter)
into MongoDB query parameters.
"""
limit = int(request_args.get('$top', 50))
skip = int(request_args.get('$skip', 0))
# Parse $orderby (e.g., $orderby=received_at desc)
sort = []
orderby_str = request_args.get('$orderby')
if orderby_str:
parts = orderby_str.split()
field = parts[0]
direction = -1 if len(parts) > 1 and parts[1].lower() == 'desc' else 1
sort.append((field, direction))
# Parse basic $filter (e.g., $filter=device_id eq '1')
mongo_filter = {}
filter_str = request_args.get('$filter')
if filter_str and ' eq ' in filter_str:
field, val = filter_str.split(' eq ')
field = field.strip()
val = val.strip().strip("'").strip('"')
# Cast data types
if val.isdigit():
val = int(val)
elif val.lower() == 'true':
val = True
elif val.lower() == 'false':
val = False
mongo_filter[field] = val
return {
"filter": mongo_filter,
"limit": limit,
"skip": skip,
"sort": sort or [("_id", -1)]
}