from flask import Flask, request import logging from logging.handlers import RotatingFileHandler import json import requests from datetime import datetime, timezone import os import re # === Robust Logging Setup === log_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') log_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), "jitbit_integration.log") file_handler = RotatingFileHandler(log_file, maxBytes=5*1024*1024, backupCount=3) file_handler.setFormatter(log_formatter) file_handler.setLevel(logging.INFO) console_handler = logging.StreamHandler() console_handler.setFormatter(log_formatter) console_handler.setLevel(logging.INFO) root_logger = logging.getLogger() root_logger.setLevel(logging.INFO) root_logger.addHandler(file_handler) root_logger.addHandler(console_handler) app = Flask(__name__) # === Jitbit Configuration === JITBIT_BASE_URL = "https://xxxxx.jitbit.com/helpdesk/api" JITBIT_USERNAME = "your service account email here" JITBIT_API_KEY = "yyyyyy" CATEGORY_NAME = "Storage Security Event" CATEGORY_CACHE = {} def sanitize_search_term(term): return re.sub(r"[^\w\-]", "", term) def get_or_create_category(name): global CATEGORY_CACHE try: response = requests.get(f"{JITBIT_BASE_URL}/categories", auth=(JITBIT_USERNAME, JITBIT_API_KEY)) logging.info(f"๐Ÿ“ฅ Category list status code: {response.status_code}") logging.info(f"๐Ÿ“œ Categories response: {response.text}") if response.status_code == 200: categories = response.json() for cat in categories: cat_name = cat.get("Name", "") cat_id = cat.get("CategoryID") if cat_name.lower() == name.lower(): CATEGORY_CACHE[name] = cat_id logging.info(f"โœ… Category '{name}' already exists with ID {cat_id}") return cat_id # Category not found, attempt to create logging.info(f"โž• Category '{name}' not found. Attempting to create it...") create_resp = requests.post( f"{JITBIT_BASE_URL}/addCategory", auth=(JITBIT_USERNAME, JITBIT_API_KEY), json={"Name": name} ) logging.info(f"๐Ÿ†• Category creation response: {create_resp.status_code} - {create_resp.text}") if create_resp.status_code == 200: new_cat = create_resp.json() new_cat_id = new_cat.get("CategoryID") if new_cat_id: CATEGORY_CACHE[name] = new_cat_id logging.info(f"โœ… Created new category '{name}' with ID {new_cat_id}") return new_cat_id else: logging.warning(f"โš ๏ธ Unexpected response structure when creating category: {new_cat}") else: logging.error(f"โŒ Failed to create category '{name}': {create_resp.status_code} - {create_resp.text}") else: logging.error(f"โŒ Failed to fetch categories: {response.status_code} - {response.text}") except Exception as e: logging.error("โŒ Exception retrieving/creating category: %s", str(e)) return "1" # Fallback to default def format_ticket_payload(payload): raw_event_id = payload.get("id", "Unknown") sanitized_event_id = sanitize_search_term(raw_event_id) user = payload.get("userName", "Unknown") client_ip = payload.get("clientIPs", ["Unknown"])[0] severity = payload.get("severity", "Unknown") state = payload.get("state", "Unknown") detected_time = payload.get("detectedTime", 0) try: timestamp = datetime.fromtimestamp(detected_time / 1000, tz=timezone.utc).isoformat() except Exception as e: timestamp = "Unknown" logging.error("Error converting detectedTime: %s", str(e)) actions = " | ".join([a.get("action", "Unknown") for a in payload.get("actions", [])]) shares = "; ".join(share.get("name", "Unknown") for share in payload.get("shares", [])) files = "; ".join(payload.get("files", [])) alert_url = payload.get("url", "Unknown") subject = f"Superna Zero Trust alert - User: {user} - Event ID: {sanitized_event_id}" body = f""" A security alert was detected: - Event ID: {raw_event_id} - User: {user} - Client IP: {client_ip} - Timestamp: {timestamp} - Severity: {severity} - State: {state} - Actions: {actions} - Shares: {shares} - Files: {files} - Alert URL: {alert_url} """ return { "Subject": subject, "Body": body.strip(), "Priority": 2 if severity.lower() == "critical" else 1, "UserEmail": JITBIT_USERNAME } def search_ticket_by_event_id(event_id): sanitized_event_id = sanitize_search_term(event_id) url = f"{JITBIT_BASE_URL}/search?query={sanitized_event_id}" auth = (JITBIT_USERNAME, JITBIT_API_KEY) try: logging.info(f"๐Ÿ” Searching for existing ticket with sanitized event ID: {sanitized_event_id}") response = requests.get(url, auth=auth) logging.info(f"๐Ÿ” Jitbit search response code: {response.status_code}") logging.info(f"๐Ÿ“„ Jitbit search response content: {response.text}") if response.status_code == 200: tickets = response.json() for ticket in tickets: ticket_id = ticket.get("IssueID") subject = ticket.get("Subject", "") if sanitize_search_term(event_id) in sanitize_search_term(subject): logging.info(f"โœ… Found matching ticket: ID={ticket_id}, Subject={subject}") return ticket_id logging.info("โ„น๏ธ No matching ticket found in search results.") else: logging.warning("โŒ Search failed: %s - %s", response.status_code, response.text) except Exception as e: logging.error("Exception during ticket search: %s", str(e)) return None def append_reply_to_ticket(ticket_id, message_body): url = f"{JITBIT_BASE_URL}/comment" auth = (JITBIT_USERNAME, JITBIT_API_KEY) params = {"id": str(ticket_id), "body": message_body} try: logging.info(f"โœ๏ธ Adding reply to existing ticket ID {ticket_id}") response = requests.post(url, auth=auth, params=params) logging.info(f"๐Ÿ“จ Jitbit reply status code: {response.status_code}") logging.info(f"๐Ÿ“ Jitbit reply response: {response.text}") if response.status_code == 200: logging.info("โœ… Reply successfully added.") return {"message": f"โœ… Reply added to existing ticket #{ticket_id}"} else: logging.warning("โŒ Failed to add reply: %s - %s", response.status_code, response.text) return {"error": f"โŒ Failed to add reply: {response.status_code} - {response.text}"} except Exception as e: logging.error("Exception during comment post: %s", str(e)) return {"error": f"โŒ Exception: {str(e)}"} def send_ticket_to_jitbit(ticket): category_id = get_or_create_category(CATEGORY_NAME) url = f"{JITBIT_BASE_URL}/ticket" auth = (JITBIT_USERNAME, JITBIT_API_KEY) form_data = { "categoryId": str(category_id), "subject": ticket["Subject"], "body": ticket["Body"], "priorityId": str(ticket["Priority"]), "suppressConfirmation": "true" } logging.info("๐Ÿ“ค Sending ticket to Jitbit with form data:") logging.info(json.dumps(form_data, indent=2)) try: response = requests.post(url, data=form_data, auth=auth) logging.info(f"โฌ…๏ธ Jitbit Response: {response.status_code} {response.text}") if response.status_code in (200, 201): return {"message": "โœ… Ticket created in Jitbit", "ticket_id": response.text} else: return {"error": f"โŒ Failed to create ticket: {response.status_code} - {response.text}"} except Exception as e: logging.error("Exception sending to Jitbit: %s", str(e)) return {"error": f"โŒ Exception: {str(e)}"} @app.route('/webhook', methods=['POST']) def webhook(): try: payload = request.get_json() logging.info("๐Ÿ“ฉ Webhook received:") logging.info(json.dumps(payload, indent=2)) ticket = format_ticket_payload(payload) event_id = payload.get("id", "Unknown") logging.info(f"๐Ÿ”Ž Processing event ID: {event_id}") existing_ticket_id = search_ticket_by_event_id(event_id) if existing_ticket_id: logging.info(f"๐Ÿงท Found existing ticket ID {existing_ticket_id}, appending reply.") result = append_reply_to_ticket(existing_ticket_id, ticket["Body"]) else: logging.info("๐Ÿ†• No existing ticket found. Creating new ticket.") result = send_ticket_to_jitbit(ticket) logging.info(f"โœ… Final webhook result: {result}") return json.dumps(result), 200 if "message" in result else 500 except Exception as e: logging.error(f"โŒ Error in webhook handler: {str(e)}") return json.dumps({"error": f"โŒ Exception in webhook handler: {str(e)}"}), 500 if __name__ == '__main__': print("๐Ÿš€ Starting Flask app on port 5000") app.run(host='0.0.0.0', port=5000, debug=True)