Shodan and jobs
Build, push image, and notify Watchtower / build-image (push) Successful in 3m25s
Build, push image, and notify Watchtower / notify (push) Successful in 13s

This commit is contained in:
2026-08-18 15:48:37 +02:00
parent 2c0776bdaa
commit 88c3de6ab7
4 changed files with 290 additions and 3 deletions
+134
View File
@@ -0,0 +1,134 @@
import os
import requests
from typing import Dict, Any, Optional
class ShodanAuditor:
"""Service class to handle Shodan API security audits and IP profiling."""
def __init__(self, api_key: Optional[str] = None):
self.api_key = api_key or os.getenv("SHODAN_API_KEY", "")
self.base_url = "https://api.shodan.io"
def get_public_ip(self) -> Optional[str]:
"""Utility method to discover the server's public IPv4 address."""
try:
res = requests.get("https://api.ipify.org?format=json", timeout=5)
return res.json().get("ip")
except Exception:
return None
def _query_host(self, ip: str) -> Dict[str, Any]:
"""Internal helper to query Shodan's Host API endpoint."""
if not self.api_key:
return {"error": "Missing Shodan API key"}
url = f"{self.base_url}/shodan/host/{ip}?key={self.api_key}"
try:
response = requests.get(url, timeout=8)
if response.status_code == 404:
return {"indexed": False, "ip": ip}
if response.status_code != 200:
return {"error": f"Shodan API error HTTP {response.status_code}"}
data = response.json()
data["indexed"] = True
return data
except requests.RequestException as e:
return {"error": str(e)}
def audit_server_vulnerabilities(self, target_ip: Optional[str] = None) -> Dict[str, Any]:
"""
Audits the server for reported CVEs, exposed services, and SSL details.
:param target_ip: IP to scan (defaults to server's own public IP)
"""
ip = target_ip or self.get_public_ip()
if not ip:
return {"status": "error", "message": "Unable to resolve target IP"}
data = self._query_host(ip)
if "error" in data:
return {"status": "error", "message": data["error"], "target_ip": ip}
if not data.get("indexed"):
return {
"status": "PASS",
"target_ip": ip,
"message": "IP is not indexed by Shodan (low public exposure)",
"vulnerabilities": [],
"open_ports": []
}
# Extract CVEs
vulns = data.get("vulns", [])
# Extract SSL/TLS info from HTTP services if available
ssl_info = []
for service in data.get("data", []):
if "ssl" in service:
ssl_details = service["ssl"]
ssl_info.append({
"port": service.get("port"),
"cert_expired": ssl_details.get("cert", {}).get("expired", False),
"cipher": ssl_details.get("cipher", {}).get("name"),
"version": ssl_details.get("versions")
})
return {
"status": "FAIL" if vulns else "PASS",
"target_ip": ip,
"hostnames": data.get("hostnames", []),
"open_ports": data.get("ports", []),
"vulnerabilities": vulns,
"vuln_count": len(vulns),
"ssl_certificates": ssl_info,
"last_shodan_update": data.get("last_update")
}
def profile_client_ip(self, client_ip: str) -> Dict[str, Any]:
"""
Profiles a incoming client (microwave) public IP address.
Checks if the client network has exposed admin panels, Telnet, or CVEs.
:param client_ip: Public IP of the connecting device/router
"""
# Skip local/private network IPs
if client_ip in ["127.0.0.1", "localhost"] or client_ip.startswith(("10.", "192.168.", "172.16.")):
return {"status": "skipped", "reason": "Local/Private IP"}
data = self._query_host(client_ip)
if "error" in data or not data.get("indexed"):
return {
"client_ip": client_ip,
"risk_level": "LOW",
"indexed": False,
"summary": "Client IP not indexed on Shodan"
}
ports = data.get("ports", [])
vulns = data.get("vulns", [])
# Check for high-risk exposed router/device ports
high_risk_ports = [23, 80, 8080, 8443, 7547] # Telnet, Web UI, TR-069
flagged_ports = [p for p in ports if p in high_risk_ports]
# Calculate basic risk level
if vulns or 23 in ports:
risk_level = "HIGH"
elif flagged_ports:
risk_level = "MEDIUM"
else:
risk_level = "LOW"
return {
"client_ip": client_ip,
"risk_level": risk_level,
"indexed": True,
"open_ports": ports,
"flagged_router_ports": flagged_ports,
"vulnerabilities": vulns,
"org": data.get("org", "Unknown ISP")
}
+70 -2
View File
@@ -7,13 +7,16 @@ import sys
import json import json
from flask import Flask, request, jsonify, current_app from flask import Flask, request, jsonify, current_app
from pymongo import MongoClient from pymongo import MongoClient
from APIs import generate, EdamamAPI 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
from APIs.shodan import ShodanAuditor
from APIs.twilio import send_alert_sms
from microwaveCookPlanner import MicrowaveCookPlanner from microwaveCookPlanner import MicrowaveCookPlanner
import safety_checker import safety_checker
import pymongo import pymongo
from APIs.twilio import send_alert_sms from apscheduler.schedulers.background import BackgroundScheduler
from jobs import job_server_cve_audit, job_client_ip_audit, job_request_telemetry
sys.path.insert(0, '..') sys.path.insert(0, '..')
try: try:
@@ -37,6 +40,9 @@ 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"] # Collection for Webex OAuth tokens
device_ip_collection = db["device_ips"] # Collection for device IPs
cve_audit_results = db["cve_audit_results"] # Collection for CVE audit results
# Webex Credentials & Configuration from Environment Variables # Webex Credentials & Configuration from Environment Variables
WEBEX_CLIENT_ID = os.getenv("WEBEX_CLIENT_ID", "YOUR_WEBEX_CLIENT_ID") WEBEX_CLIENT_ID = os.getenv("WEBEX_CLIENT_ID", "YOUR_WEBEX_CLIENT_ID")
@@ -63,6 +69,44 @@ webex_manager = WebexManager(
team_id=WEBEX_TEAM_ID, team_id=WEBEX_TEAM_ID,
user_id=WEBEX_NINLUC_ID user_id=WEBEX_NINLUC_ID
) )
shodan_auditor = ShodanAuditor()
# -------------------------------------------------------------
# Cron Scheduler Setup
# -------------------------------------------------------------
scheduler = BackgroundScheduler(daemon=True)
# Job 1: Server CVE audit every night at 00:00 UTC
scheduler.add_job(
func=job_server_cve_audit,
args=[shodan_auditor, db],
trigger="cron",
hour=0,
minute=0,
id="server_cve_audit_job"
)
# Job 2: Client IP Shodan audit every 6 hours
scheduler.add_job(
func=job_client_ip_audit,
args=[shodan_auditor, db],
trigger="cron",
hour="*/6",
minute=15,
id="client_ip_audit_job"
)
# Job 3: Request telemetry runs AFTER the client audit (e.g., every 12 hours)
scheduler.add_job(
func=job_request_telemetry,
args=[send_command],
trigger="cron",
hour="1,13",
minute=0,
id="request_telemetry_job"
)
scheduler.start()
# --------------------------------------------------------- # ---------------------------------------------------------
# OAuth Routes # OAuth Routes
@@ -97,6 +141,14 @@ def hello_world():
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")
client_ip = request.headers.get('X-Forwarded-For', request.remote_addr).split(',')[0].strip()
device_ip_collection.insert_one({
"microwave_id": microwave_id,
"client_ip": client_ip,
"received_at": datetime.now(timezone.utc)
})
if not data: if not data:
return jsonify({"error": "Invalid or missing JSON payload"}), 400 return jsonify({"error": "Invalid or missing JSON payload"}), 400
@@ -326,6 +378,22 @@ def telemetry():
except Exception as e: except Exception as e:
return jsonify({"error": f"Database error: {str(e)}"}), 500 return jsonify({"error": f"Database error: {str(e)}"}), 500
# -------------------------------------------------------------
# Server CVE & SSL Audit Endpoint
# -------------------------------------------------------------
@app.route('/api/security/cve-audit', methods=['GET'])
def run_cve_audit():
"""Runs a vulnerability and SSL audit against this server."""
audit_results = shodan_auditor.audit_server_vulnerabilities()
cve_audit_results.insert_one({
"audit_results": audit_results,
"timestamp": datetime.now(timezone.utc)
})
status_code = 200 if audit_results["status"] == "PASS" else 409
return jsonify(audit_results), status_code
@app.route("/debug/telemetryrequest", methods=["GET"]) @app.route("/debug/telemetryrequest", methods=["GET"])
def debug_telemetryrequest(): def debug_telemetryrequest():
+84
View File
@@ -0,0 +1,84 @@
import os
import time
from datetime import datetime, timezone, timedelta
# Fallback base URL since 'request.host_url' is not available in background threads
SERVER_BASE_URL = os.getenv("SERVER_BASE_URL", "https://smartwave.matthiasg.dev")
def job_server_cve_audit(shodan_auditor, db):
"""Job 1: Audits Flask server public IP for CVEs and saves to MongoDB."""
print("[Cron] Starting Server CVE Audit...")
audit_results = shodan_auditor.audit_server_vulnerabilities()
db.cve_audit_results.insert_one({
"audit_results": audit_results,
"timestamp": datetime.now(timezone.utc)
})
print(f"[Cron] Server CVE Audit finished. Status: {audit_results.get('status')}")
def job_client_ip_audit(shodan_auditor, db):
"""
Job 2: Fetches distinct client IPs logged in the last 24h and audits them.
Deduplicates IPs and applies rate limits to protect Shodan API credits.
"""
print("[Cron] Starting Client IP Security Audit...")
pipeline = [
{"$group": {
"_id": "$client_ip",
"microwave_ids": {"$addToSet": "$microwave_id"},
"last_seen": {"$max": "$received_at"}
}}
]
recent_clients = list(db.device_ip_collection.aggregate(pipeline))
for entry in recent_clients:
client_ip = entry["_id"]
# Skip local/private IPs
if not client_ip or client_ip in ["127.0.0.1", "localhost"] or client_ip.startswith(("10.", "192.168.", "172.16.")):
continue
profile = shodan_auditor.profile_client_ip(client_ip)
# Upsert client profile in a separate collection
db.client_audit_results.update_one(
{"client_ip": client_ip},
{
"$set": {
"client_ip": client_ip,
"microwave_ids": entry["microwave_ids"],
"last_seen": entry["last_seen"],
"profile": profile,
"audited_at": datetime.now(timezone.utc)
}
},
upsert=True
)
# Pause 1 second to respect Shodan API rate limits (1 req/sec)
time.sleep(1)
print(f"[Cron] Client IP Security Audit completed for {len(recent_clients)} IPs.")
def job_request_telemetry(send_command_func):
"""Job 3: Triggers MQTT command to request telemetry from all microwaves."""
print("[Cron] Sending MQTT Telemetry Request to cmd/all...")
telemetry_url = f"{SERVER_BASE_URL.rstrip('/')}/telemetry"
cmd_payload = {
"action": "request_telemetry",
"endpoint": telemetry_url
}
try:
send_command_func(topic="cmd/all", payload=cmd_payload)
print("[Cron] Telemetry request successfully published.")
except Exception as e:
print(f"[Cron Error] Failed to publish MQTT command: {str(e)}")
+2 -1
View File
@@ -6,4 +6,5 @@ requests==2.32.3
paho-mqtt>=1.6,<3 paho-mqtt>=1.6,<3
openai>=1.0.0 openai>=1.0.0
pydantic>=2.0.0 pydantic>=2.0.0
twilio>=9.0.0 twilio>=9.0.0
APScheduler>=3.10.4,<4.0.0