Shodan and jobs
This commit is contained in:
@@ -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
@@ -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():
|
||||||
|
|||||||
@@ -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)}")
|
||||||
@@ -7,3 +7,4 @@ 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
|
||||||
Reference in New Issue
Block a user