from flask import Flask, request, jsonify import requests import json from datetime import datetime import urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) app = Flask(__name__) # === FortiAnalyzer Config === FAZ_URL = "https://x.x.x.x/jsonrpc" FAZ_API_KEY = "xxxxxx" VERIFY_SSL = False MAX_FILE_COUNT = 5 # === Fortinet-Aligned Severity Remap (enabled) === ENABLE_SEVERITY_REMAP = True SEVERITY_REMAP = { "critical": "critical", "major": "high", "warning": "medium", "info": "low" } # === Helpers === def extract_share_names(payload: dict): shares = payload.get("shares", []) if not isinstance(shares, list): return [] formatted = [] for s in shares: if not isinstance(s, dict): continue name = s.get("name", "unknown") path = s.get("path", "unknown path") ne_name = s.get("neName", "") if ne_name: formatted.append(f"{name} ({ne_name}:{path})") else: formatted.append(f"{name} ({path})") return formatted def extract_files(payload: dict): files = payload.get("files", []) truncated = files[:MAX_FILE_COUNT] if len(files) > MAX_FILE_COUNT: truncated.append(f"...{len(files) - MAX_FILE_COUNT} more") return truncated def map_severity(sev: str): raw = sev.lower() if sev else "info" if ENABLE_SEVERITY_REMAP and raw in SEVERITY_REMAP: mapped = SEVERITY_REMAP[raw] return mapped, raw return raw, None def create_incident(payload: dict): """Check for existing incident before creating a new one in FortiAnalyzer via JSON-RPC API.""" severity_raw = payload.get("severity", "info") severity, raw_label = map_severity(severity_raw) shares = extract_share_names(payload) files = extract_files(payload) user = payload.get("userName", "unknown") client_ips = payload.get("clientIPs", []) client_ip = client_ips[0] if isinstance(client_ips, list) and client_ips else payload.get("clientIP", "unknown") # Normalize Alert ID (remove colon) alert_id_raw = str(payload.get("id", "unknown")) alert_id = alert_id_raw.replace(":", "") if alert_id_raw else "unknown" incident_category = "Malicious Code" incident_name = f"Storage Threat Detection against user name {user} from host {client_ip}, Alert ID {alert_id}" shares_text = "\n".join(f" - {s}" for s in shares) if shares else "None" files_text = "\n".join(f" - {f}" for f in files) if files else "None" description = ( f"User: {user}\n" f"Client IP: {client_ip}\n" f"Alert ID: {alert_id}\n" f"SMB Shares:\n{shares_text}\n" f"Files:\n{files_text}" ) headers = { "Authorization": f"Bearer {FAZ_API_KEY}", "Content-Type": "application/json" } # === Step 1: Search for existing incidents by Alert ID === search_payload = { "id": 1, "jsonrpc": "2.0", "method": "get", "params": [ { "apiver": 3, "url": "/incidentmgmt/adom/root/incidents", "filter": f"name=@Alert ID {alert_id}", "limit": 10, "offset": 0, "detail-level": "basic" } ] } print(f"[DEBUG] Searching for existing incident with Alert ID {alert_id}") print(f"[DEBUG] Search JSON-RPC Payload:\n{json.dumps(search_payload, indent=2)}") existing_incid = None found_data = False try: search_resp = requests.post(FAZ_URL, headers=headers, json=search_payload, verify=VERIFY_SSL) print(f"[DEBUG] FAZ Search Response Code: {search_resp.status_code}") try: search_json = search_resp.json() print(f"[DEBUG] FAZ Search Response Body:\n{json.dumps(search_json, indent=2)}") except Exception: search_json = {} print(f"[DEBUG] Raw FAZ Search Response:\n{search_resp.text}") # Parse search result result_obj = search_json.get("result") if result_obj: if isinstance(result_obj, list) and len(result_obj) > 0: result_data = result_obj[0] elif isinstance(result_obj, dict): result_data = result_obj else: result_data = {} status = result_data.get("status", {}) message = status.get("message", "").lower() data_list = result_data.get("data", []) if isinstance(data_list, list) and len(data_list) > 0: existing_incid = data_list[0].get("incid") found_data = True elif "no data" in message: found_data = False if found_data and existing_incid: print(f"[INFO] Existing incident found: {existing_incid}") return 200, { "result": { "incid": existing_incid, "message": "Duplicate incident ignored" } } else: print(f"[INFO] No existing incident found (status='no data'), creating new one") except Exception as e: print(f"[ERROR] Search query failed: {e}") existing_incid = None found_data = False # === Step 2: Create new incident if none found === faz_payload = { "id": 1, "jsonrpc": "2.0", "method": "add", "params": [ { "apiver": 3, "url": "/incidentmgmt/adom/root/incident", "reporter": "Superna Data Security Edition", "endpoint": user, "category": incident_category, "description": description, "severity": severity, "status": "draft", "name": incident_name, "mitre_technique_id": "T1486" } ] } print("\n=== Event Processing ===") print(f"Severity (raw): {severity_raw}") print(f"Severity (Fortinet JSON-RPC): {severity}") print(f"Client IP: {client_ip}") print(f"Alert ID: {alert_id}") print(f"Shares: {shares if shares else 'None'}") print(f"Files: {files if files else 'None'}") print("Incident JSON-RPC Payload:") print(json.dumps(faz_payload, indent=2)) print("========================") resp = requests.post(FAZ_URL, headers=headers, json=faz_payload, verify=VERIFY_SSL) print(f"[INFO] FAZ Create Response Code: {resp.status_code}") try: data = resp.json() print(f"[INFO] FAZ Response Body:\n{json.dumps(data, indent=2)}") except Exception: data = {"error": resp.text} print(f"[INFO] Raw Response:\n{resp.text}") return resp.status_code, data # === Flask Routes === @app.route("/webhook", methods=["POST"]) def webhook(): try: payload = request.json or {} print("[INFO] Incoming webhook received") status_code, data = create_incident(payload) incident_id = None try: if "result" in data and isinstance(data["result"], dict): incident_id = data["result"].get("incid") elif "result" in data and isinstance(data["result"], list): result_item = data["result"][0].get("data") or {} incident_id = result_item.get("incid") except Exception: incident_id = None if status_code == 200 and incident_id: return jsonify({"status": "success", "incident_id": incident_id}), 200 else: return jsonify({"status": "error", "response": data}), 500 except Exception as e: print(f"[ERROR] Exception: {e}") return jsonify({"status": "error", "message": str(e)}), 500 @app.route("/healthz") def healthz(): return "ok", 200 if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=True)