#!/usr/bin/env python3 """IP-DB Sensor - multi-profile honeypot for https://ip-db.com Usage: ipdb-sensor.py run the sensor ipdb-sensor.py --check verify the API key ipdb-sensor.py --config print the effective configuration ipdb-sensor.py --list-profiles list the decoy profiles (add --json for machine output) ipdb-sensor.py --version print the version Environment: IPDB_KEY API key (required) IPDB_SENSOR_NAME sensor name, default: hostname IPDB_PROFILES decoys to run: keys or @bundles, each optionally key:port e.g. "exchange,fortigate:10443,@databases" (default: @classic) IPDB_PORTS extra raw TCP ports IPDB_IGNORE comma-separated IPs/CIDRs to never report IPDB_TLS_CERT certificate for TLS decoys (default /opt/ipdb-sensor/tls/sensor.crt) IPDB_TLS_KEY its key (default /opt/ipdb-sensor/tls/sensor.key) IPDB_FAIL2BAN_LOG path to fail2ban.log IPDB_LOG_LEVEL debug | info | warning | error IPDB_ENDPOINT report URL Privacy: decoys never store or send passwords, user names or payloads. A login attempt or a known exploit pattern is reported as a tag on the event, nothing more. Licence: MIT """ import argparse import asyncio import email.utils import html import ipaddress import json import logging import os import random import re import signal import socket import ssl import struct import sys import time import urllib.error import urllib.parse import urllib.request VERSION = "2.1.0" DEFAULT_ENDPOINT = "https://ip-db.com/api/v1/sensor/events" REGISTER_URL = "https://ip-db.com/en/account/register" USER_AGENT = "ipdb-sensor/%s (+https://ip-db.com/en/honeypot)" % VERSION DEFAULT_CERT = "/opt/ipdb-sensor/tls/sensor.crt" DEFAULT_KEY = "/opt/ipdb-sensor/tls/sensor.key" FLUSH_SECONDS = 60 MAX_BACKOFF = 900 POSTS_PER_FLUSH = 3 MAX_PENDING = 20000 MAX_BATCH = 2000 READ_BYTES = 2048 READ_TIMEOUT = 8.0 SESSION_SECONDS = 30.0 MAX_HEADER = 16384 MAX_BODY = 16384 MAX_CONCURRENT = 256 MAX_PER_SOURCE = 4 STATS_SECONDS = 3600 log = logging.getLogger("ipdb-sensor") CATEGORIES = [ ("edge", "VPN gateways & firewalls"), ("mail", "Mail & collaboration"), ("web", "Web applications & panels"), ("devops", "DevOps, cloud & APIs"), ("db", "Databases & caches"), ("remote", "Remote access & file transfer"), ("iot", "IoT, routers & industrial"), ] def field(label, name, kind="text"): return (label, name, kind) USER = field("Username", "username") PASS = field("Password", "password", "password") EMAIL = field("Email", "email", "email") def http(key, name, cat, port, title, server="nginx", tls=False, login=None, routes=(), headers=(), home=None, fields=(USER, PASS), button="Sign in", accent="#1f6feb", note="", basic=None, notfound=None, desc="", action=None, layout="card", subtitle="", accent2=None, brand=None): if action is None and login: first = login.split("|")[0] action = "/" if "(" in first else re.sub(r"[\\^$?]", "", first) return {"key": key, "name": name, "cat": cat, "port": port, "kind": "http", "tls": tls, "title": title, "server": server, "login": login, "action": action or "/", "routes": list(routes), "headers": list(headers), "home": home, "fields": list(fields), "button": button, "accent": accent, "note": note, "basic": basic, "notfound": notfound, "desc": desc, "layout": layout, "subtitle": subtitle, "accent2": accent2, "brand": brand} def tcp(key, name, cat, port, dialogue, desc=""): return {"key": key, "name": name, "cat": cat, "port": port, "kind": "tcp", "tls": False, "dialogue": dialogue, "desc": desc} IIS = "Microsoft-IIS/10.0" APACHE = "Apache" JSON = "application/json" XML = "text/xml; charset=utf-8" PROFILES = [ http("exchange", "Microsoft Exchange (OWA)", "mail", 443, "Outlook", server=IIS, tls=True, login=r"^/owa/auth\.owa$", home="/owa/auth/logon.aspx?url=https%3a%2f%2f{host}%2fowa%2f&reason=0", fields=(field("Domain\\user name", "username"), PASS), accent="#0f6cbd", layout="stacked", brand="Outlook", subtitle="Sign in to your account", headers=(("X-OWA-Version", "15.2.1544.4"), ("X-AspNet-Version", "4.0.30319"), ("X-Powered-By", "ASP.NET"), ("X-FEServer", "EXCH01")), routes=(("*", r"^/owa/?$", ("redirect", "/owa/auth/logon.aspx?url=https%3a%2f%2f{host}%2fowa%2f&reason=0")), ("*", r"^/owa/auth/logon\.aspx", ("login",)), ("*", r"^/ecp/?", ("redirect", "/owa/auth/logon.aspx?url=https%3a%2f%2f{host}%2fecp%2f&reason=0")), ("*", r"^/(autodiscover/autodiscover\.xml|ews/|mapi/|rpc/|microsoft-server-activesync|oab/)", ("basic", "{host}")), ("*", r"^/autodiscover/autodiscover\.json", ("status", 401))), desc="OWA sign-in, ECP, EWS, Autodiscover and ActiveSync endpoints; flags ProxyLogon/ProxyShell probes"), http("fortigate", "Fortinet FortiGate SSL-VPN", "edge", 10443, "Please Login", server="xxxxxxxx-xxxxx", tls=True, login=r"^/remote/logincheck$", home="/remote/login?lang=en", accent="#da291c", button="Login", layout="console", brand="FortiGate", subtitle="SSL-VPN Portal", note="FortiOS 7.2", routes=(("*", r"^/remote/login", ("login",)), ("*", r"^/remote/fgt_lang", ("status", 403)), ("*", r"^/api/v2/", ("json", 401, '{"http_method":"GET","status":"error","http_status":401}'))), desc="SSL-VPN portal and REST API; flags CVE-2018-13379 and CVE-2022-40684"), http("globalprotect", "Palo Alto GlobalProtect", "edge", 4443, "GlobalProtect Portal", server="", tls=True, login=r"^/global-protect/(login|getconfig)\.esp$", action="/global-protect/login.esp", home="/global-protect/login.esp", accent="#fa582d", layout="console", brand="GlobalProtect", subtitle="Please enter your credentials", button="LOG IN", routes=(("*", r"^/global-protect/login\.esp", ("login",)), ("*", r"^/(global-protect|ssl-vpn)/prelogin\.esp", ("text", 200, XML, 'Success' 'false' 'Enter login credentials' 'UsernamePassword' '1EU')), ("*", r"^/ssl-vpn/hipreport\.esp", ("status", 512)), ("*", r"^/php/login\.php", ("login",))), desc="Portal, prelogin and HIP report endpoints; flags CVE-2024-3400, CVE-2024-0012, CVE-2025-0108"), http("netscaler", "Citrix NetScaler Gateway", "edge", 8443, "NetScaler AAA", server="", tls=True, login=r"^/(cgi/login|p/u/doauthentication\.do|nf/auth/doauthentication\.do)$", action="/cgi/login", home="/logon/LogonPoint/index.html", accent="#452170", button="Log On", layout="console", brand="Citrix", subtitle="Please log on to continue", routes=(("*", r"^/logon/logonpoint/", ("login",)), ("*", r"^/vpn/index\.html", ("login",)), ("*", r"^/oauth/idp/\.well-known/openid-configuration", ("json", 200, '{"issuer":"https://{host}/oauth/idp","authorization_endpoint":"https://{host}/oauth/idp/login"}'))), headers=(("X-Frame-Options", "SAMEORIGIN"),), desc="Gateway logon point and OAuth IdP; flags CVE-2019-19781 and Citrix Bleed (CVE-2023-4966)"), http("ivanti", "Ivanti Connect Secure", "edge", 8444, "Ivanti Connect Secure", server="", tls=True, login=r"^/dana-na/auth/url_default/login\.cgi$", home="/dana-na/auth/url_default/welcome.cgi", accent="#d9272e", layout="console", brand="Ivanti", subtitle="Welcome. Please sign in.", routes=(("*", r"^/dana-na/auth/url_default/welcome\.cgi", ("login",)), ("*", r"^/api/v1/", ("json", 403, '{"result":"Forbidden"}'))), desc="Pulse/Ivanti sign-in pages and API; flags CVE-2019-11510, CVE-2023-46805, CVE-2024-21887"), http("cisco-asa", "Cisco ASA AnyConnect", "edge", 9443, "SSL VPN Service", server="", tls=True, login=r"^/\+webvpn\+/index\.html$", home="/+CSCOE+/logon.html", accent="#049fd9", button="Login", layout="console", brand="AnyConnect", subtitle="SSL VPN Service", fields=(field("Username", "username"), field("Password", "password", "password"), field("Group", "group_list")), routes=(("*", r"^/\+cscoe\+/logon\.html", ("login",)), ("*", r"^/\+cscot\+/", ("status", 404))), headers=(("X-Transcend-Version", "1"),), desc="WebVPN logon and translation tables; flags CVE-2020-3452"), http("f5-bigip", "F5 BIG-IP TMUI", "edge", 8445, "BIG-IP®- Redirect", server="BigIP", tls=True, login=r"^/tmui/logmein\.html$", home="/tmui/login.jsp", accent="#e4002b", button="Log in", layout="console", brand="BIG-IP", subtitle="Configuration Utility", routes=(("*", r"^/tmui/login\.jsp$", ("login",)), ("*", r"^/mgmt/", ("json", 401, '{"code":401,"message":"Authorization failed: no user authentication header or token detected.","errorStack":[]}'))), desc="TMUI login and iControl REST; flags CVE-2020-5902, CVE-2022-1388, CVE-2023-46747"), http("sonicwall", "SonicWall SSL-VPN", "edge", 4433, "SonicWall - Virtual Office", server="SonicWALL SSL-VPN Web Server", tls=True, login=r"^/cgi-bin/userlogin$", home="/cgi-bin/welcome", accent="#ff791a", layout="console", brand="SonicWall", subtitle="Virtual Office", routes=(("*", r"^/cgi-bin/welcome", ("login",)), ("*", r"^/sslvpnlogin\.html", ("login",))), desc="Virtual Office login"), http("checkpoint", "Check Point Mobile Access", "edge", 8446, "Check Point Mobile", server="CPWS", tls=True, login=r"^/clients/mycrl$", home="/sslvpn/Login/Login", accent="#e9479a", layout="console", brand="Check Point", subtitle="Mobile Access Portal", routes=(("*", r"^/sslvpn/login/", ("login",)),), desc="SSL VPN portal; flags CVE-2024-24919"), http("pfsense", "pfSense / OPNsense web GUI", "edge", 8447, "pfSense - Login", server="nginx", tls=True, login=r"^/(index\.php)?$", action="/index.php", home=None, accent="#212121", button="Sign In", fields=(field("Username", "usernamefld"), field("Password", "passwordfld", "password")), routes=(("GET", r"^/(index\.php)?$", ("login",)),), desc="Firewall admin login"), http("vcenter", "VMware vCenter / ESXi", "devops", 9443, "VMware vSphere", server="envoy", tls=True, login=r"^/(ui/login|websso/saml2/sso/.*|sdk)$", action="/ui/login", home="/ui/", accent="#1d428a", button="LOGIN", layout="split", brand="vSphere", subtitle="VMware vCenter Single Sign-On", routes=(("GET", r"^/ui/?$", ("login",)), ("*", r"^/(sdk|mob)", ("basic", "VMware HTTP server"))), desc="vSphere client, SSO and SDK; flags CVE-2021-21972, CVE-2021-21985, CVE-2021-22005"), http("proxmox", "Proxmox VE", "devops", 8006, "pve - Proxmox Virtual Environment", server="pve-api-daemon/3.0", tls=True, login=r"^/api2/(json|extjs)/access/ticket$", action="/api2/json/access/ticket", home=None, accent="#e57000", button="Login", layout="console", brand="Proxmox VE", subtitle="Proxmox Virtual Environment", routes=(("GET", r"^/$", ("login",)), ("*", r"^/api2/", ("json", 401, '{"data":null}'))), desc="Web UI and API ticket endpoint"), http("wordpress", "WordPress", "web", 8080, "Log In ‹ Site — WordPress", server=APACHE, login=r"^/(wp-login\.php|xmlrpc\.php)$", action="/wp-login.php", home=None, accent="#2271b1", button="Log In", layout="stacked", brand="WordPress", fields=(field("Username or Email Address", "log"), field("Password", "pwd", "password")), headers=(("X-Powered-By", "PHP/8.1.27"), ("Link", '; rel="https://api.w.org/"')), routes=(("GET", r"^/$", ("text", 200, "text/html; charset=UTF-8", "Site

Just another WordPress site

")), ("GET", r"^/wp-login\.php", ("login",)), ("*", r"^/wp-admin", ("redirect", "/wp-login.php?redirect_to=%2Fwp-admin%2F&reauth=1")), ("GET", r"^/xmlrpc\.php", ("text", 405, "text/plain", "XML-RPC server accepts POST requests only.")), ("*", r"^/wp-json/wp/v2/users", ("json", 200, '[{"id":1,"name":"admin","slug":"admin"}]')), ("*", r"^/wp-json", ("json", 200, '{"name":"Site","namespaces":["oembed/1.0","wp/v2"]}')), ("*", r"^/readme\.html", ("text", 200, "text/html", "WordPress › ReadMe

WordPress

"))), desc="wp-login, xmlrpc, REST API and readme; xmlrpc brute force counts as login attempts"), http("phpmyadmin", "phpMyAdmin", "web", 8180, "phpMyAdmin", server=APACHE, login=r"^/(phpmyadmin/)?index\.php$", action="/phpmyadmin/index.php", home="/phpmyadmin/", accent="#6c78af", button="Go", layout="minimal", brand="phpMyAdmin", subtitle="Welcome to phpMyAdmin", fields=(field("Username", "pma_username"), field("Password", "pma_password", "password")), routes=(("GET", r"^/(phpmyadmin|pma|mysql)/?(index\.php)?$", ("login",)),), desc="Login page under the usual paths"), http("cpanel", "cPanel & WHM", "web", 2083, "cPanel Login", server="cpsrvd", tls=True, login=r"^/login/?$", home=None, accent="#ff6c2c", button="Log in", routes=(("GET", r"^/$", ("login",)),), desc="cPanel login (use 2087 for WHM)"), http("plesk", "Plesk", "web", 8443, "Plesk Obsidian", server="sw-cp-server", tls=True, login=r"^/login_up\.php", home="/login_up.php", accent="#52bbe6", button="Log in", fields=(field("Username", "login_name"), field("Password", "passwd", "password")), routes=(("GET", r"^/login_up\.php", ("login",)),), desc="Panel login"), http("webmin", "Webmin", "web", 10000, "Login to Webmin", server="MiniServ/2.105", tls=True, login=r"^/session_login\.cgi$", home=None, accent="#3c763d", button="Sign in", fields=(field("Username", "user"), field("Password", "pass", "password")), routes=(("GET", r"^/$", ("login",)), ("*", r"^/password_change\.cgi", ("status", 200))), desc="Login and password change; flags CVE-2019-15107"), http("tomcat", "Apache Tomcat Manager", "web", 8081, "Apache Tomcat/9.0.85", server="", basic="Tomcat Manager Application", home=None, routes=(("GET", r"^/$", ("text", 200, "text/html;charset=UTF-8", "Apache Tomcat/9.0.85

Apache Tomcat/9.0.85

If you're seeing this, you've successfully installed Tomcat. Congratulations!

")), ("*", r"^/(manager|host-manager)/", ("basic", "Tomcat Manager Application"))), notfound="tomcat", desc="Manager and host-manager behind Basic auth; flags CVE-2017-12617"), http("jenkins", "Jenkins", "devops", 8082, "Sign in [Jenkins]", server="Jetty(10.0.18)", login=r"^/j_spring_security_check$", home="/login?from=%2F", accent="#335061", button="Sign in", fields=(field("Username", "j_username"), field("Password", "j_password", "password")), headers=(("X-Jenkins", "2.426.3"), ("X-Hudson", "1.395")), routes=(("GET", r"^/login", ("login",)), ("*", r"^/(script|manage|asynchpeople)", ("redirect", "/login?from=%2Fmanage"))), desc="Login, script console and CLI; flags CVE-2024-23897"), http("gitlab", "GitLab", "devops", 8929, "Sign in ยท GitLab", server="nginx", login=r"^/users/sign_in$", home="/users/sign_in", accent="#fc6d26", button="Sign in", layout="split", brand="GitLab", subtitle="A complete DevOps platform", fields=(field("Username or primary email", "user[login]"), field("Password", "user[password]", "password")), routes=(("GET", r"^/users/sign_in", ("login",)), ("*", r"^/api/v4/", ("json", 401, '{"message":"401 Unauthorized"}'))), desc="Sign-in and API; flags CVE-2021-22205"), http("grafana", "Grafana", "devops", 3000, "Grafana", server="", login=r"^/login$", home="/login", accent="#f46800", button="Log in", layout="console", brand="Grafana", subtitle="Welcome to Grafana", fields=(field("Email or username", "user"), PASS), routes=(("GET", r"^/login", ("login",)), ("*", r"^/api/", ("json", 401, '{"message":"Unauthorized"}'))), desc="Login and API; flags CVE-2021-43798"), http("kibana", "Kibana", "devops", 5601, "Elastic", server="", login=r"^/internal/security/login$", home="/login", accent="#00bfb3", button="Log in", layout="console", brand="Elastic", subtitle="Welcome to Elastic", headers=(("kbn-name", "kibana"), ("kbn-license-sig", "x")), routes=(("GET", r"^/login", ("login",)), ("*", r"^/api/status", ("json", 401, '{"statusCode":401,"error":"Unauthorized"}'))), desc="Login and status API; flags CVE-2019-7609"), http("confluence", "Atlassian Confluence", "devops", 8090, "Log In - Confluence", server="", login=r"^/(dologin\.action|rest/tsv/1\.0/authenticate)$", action="/dologin.action", home="/login.action", accent="#0052cc", button="Log in", fields=(field("Username", "os_username"), field("Password", "os_password", "password")), routes=(("GET", r"^/login\.action", ("login",)),), desc="Login; flags CVE-2021-26084, CVE-2022-26134, CVE-2023-22515, CVE-2023-22527"), http("jira", "Atlassian Jira", "devops", 8091, "Log in - Jira", server="", login=r"^/(login\.jsp|rest/auth/1/session)$", action="/login.jsp", home="/login.jsp", accent="#0052cc", button="Log In", fields=(field("Username", "os_username"), field("Password", "os_password", "password")), routes=(("GET", r"^/login\.jsp", ("login",)), ("*", r"^/rest/api/", ("json", 401, '{"errorMessages":["You are not authenticated."]}'))), desc="Login and REST API"), http("teamcity", "JetBrains TeamCity", "devops", 8111, "Log in to TeamCity", server="", login=r"^/loginsubmit\.html$", home="/login.html", accent="#000000", button="Log in", routes=(("GET", r"^/login\.html", ("login",)), ("*", r"^/app/rest/", ("text", 401, "text/plain", "Authentication required"))), desc="Login and REST API; flags CVE-2023-42793, CVE-2024-27198"), http("spring-boot", "Spring Boot Actuator", "devops", 8888, "", server="", home=None, routes=(("GET", r"^/$", ("json", 404, '{"timestamp":"{now}","status":404,"error":"Not Found","path":"/"}')), ("*", r"^/actuator/?$", ("json", 200, '{"_links":{"self":{"href":"http://{host}/actuator"},"health":{"href":"http://{host}/actuator/health"},"env":{"href":"http://{host}/actuator/env"}}}')), ("*", r"^/actuator/health", ("json", 200, '{"status":"UP"}')), ("*", r"^/actuator/", ("json", 401, '{"status":401,"error":"Unauthorized"}'))), notfound="spring", desc="Actuator endpoints; flags Spring4Shell and Spring Cloud Function RCE"), http("docker", "Docker Engine API", "devops", 2375, "", server="Docker/24.0.7 (linux)", home=None, routes=(("*", r"^/(v[\d.]+/)?version", ("json", 200, '{"Version":"24.0.7","ApiVersion":"1.43","Os":"linux","Arch":"amd64","KernelVersion":"5.15.0-91-generic"}')), ("*", r"^/(v[\d.]+/)?_ping", ("text", 200, "text/plain", "OK")), ("*", r"^/(v[\d.]+/)?(containers|images)/json", ("json", 200, "[]")), ("POST", r"^/(v[\d.]+/)?(containers/create|images/create|exec)", ("json", 500, '{"message":"no space left on device"}'))), headers=(("Api-Version", "1.43"), ("Ostype", "linux")), notfound="json", desc="Unauthenticated Docker API; container creation is flagged as abuse"), http("kubernetes", "Kubernetes API server", "devops", 6443, "", server="", tls=True, home=None, routes=(("*", r"^/version", ("json", 200, '{"major":"1","minor":"28","gitVersion":"v1.28.5"}')), ("*", r"^/", ("json", 403, '{"kind":"Status","apiVersion":"v1","status":"Failure","message":"forbidden: User \\"system:anonymous\\" cannot get path \\"{path}\\"","reason":"Forbidden","code":403}'))), desc="Anonymous API access"), http("etcd", "etcd", "devops", 2379, "", server="", home=None, routes=(("*", r"^/version", ("json", 200, '{"etcdserver":"3.5.10","etcdcluster":"3.5.0"}')), ("*", r"^/v2/keys", ("json", 401, '{"message":"Insufficient credentials"}'))), notfound="json", desc="Version and key-space endpoints"), http("elasticsearch", "Elasticsearch", "db", 9200, "", server="", basic="security", home=None, routes=(("GET", r"^/$", ("basic", "security")), ("*", r"^/(_cat|_search|_cluster|_nodes|_all)", ("basic", "security"))), headers=(("X-elastic-product", "Elasticsearch"),), desc="REST API behind Basic auth; flags script-injection exploits"), http("couchdb", "Apache CouchDB", "db", 5984, "", server="CouchDB/3.3.3 (Erlang OTP/24)", home=None, routes=(("GET", r"^/$", ("json", 200, '{"couchdb":"Welcome","version":"3.3.3","vendor":{"name":"The Apache Software Foundation"}}')), ("*", r"^/_session", ("json", 401, '{"error":"unauthorized","reason":"Name or password is incorrect."}')), ("*", r"^/", ("json", 401, '{"error":"unauthorized","reason":"You are not a server admin."}'))), login=r"^/_session$", desc="Welcome and session endpoints"), http("winrm", "Windows Remote Management", "remote", 5985, "", server="Microsoft-HTTPAPI/2.0", basic="WSMAN", home=None, routes=(("*", r"^/wsman", ("basic", "WSMAN")),), desc="WS-Management endpoint behind Negotiate/Basic"), http("synology", "Synology DSM", "iot", 5000, "Synology DiskStation", server="nginx", login=r"^/webapi/(auth\.cgi|entry\.cgi)$", action="/webapi/auth.cgi", home=None, accent="#2a2a2a", button="Sign In", routes=(("GET", r"^/$", ("login",)),), desc="DiskStation Manager login"), http("hikvision", "Hikvision IP camera", "iot", 8000, "", server="App-webs/", login=r"^/isapi/security/sessionlogin", action="/ISAPI/Security/sessionLogin", home="/doc/page/login.asp", accent="#c8102e", button="Login", routes=(("GET", r"^/doc/page/login\.asp", ("login",)), ("*", r"^/isapi/", ("text", 401, XML, '401Unauthorized'))), desc="Camera web login and ISAPI; flags CVE-2021-36260"), http("router", "Home router / ONT admin", "iot", 8089, "Login", server="Boa/0.94.14rc21", login=r"^/(boaform/admin/formlogin|goform/login|cgi-bin/luci|login\.cgi)$", action="/boaform/admin/formLogin", home=None, accent="#3b5998", button="Login", routes=(("GET", r"^/$", ("login",)),), desc="Generic router admin; flags GPON, TP-Link and Huawei exploit paths"), http("admin-panel", "Generic admin panel (Basic auth)", "web", 8083, "", server="nginx", basic="admin", home=None, routes=(("*", r"^/", ("basic", "admin")),), desc="Any path asks for Basic credentials"), http("weblogic", "Oracle WebLogic", "web", 7001, "Oracle WebLogic Server Administration Console", server="", login=r"^/console/j_security_check$", home="/console/login/LoginForm.jsp", accent="#c74634", button="Login", fields=(field("Username", "j_username"), field("Password", "j_password", "password")), routes=(("GET", r"^/console/login/", ("login",)), ("*", r"^/wls-wsat/", ("status", 500))), desc="Admin console and WLS-WSAT; flags CVE-2020-14882"), http("activemq-web", "Apache ActiveMQ console", "devops", 8161, "", server="Jetty(9.4.53.v20231009)", basic="ActiveMQRealm", home=None, routes=(("*", r"^/", ("basic", "ActiveMQRealm")),), desc="Web console behind Basic auth"), http("openfire", "Openfire admin", "devops", 9090, "Openfire Admin Console", server="", login=r"^/login\.jsp$", home="/login.jsp", accent="#11779e", button="Login", routes=(("GET", r"^/login\.jsp", ("login",)),), desc="Admin console; flags CVE-2023-32315"), http("roundcube", "Roundcube Webmail", "mail", 8084, "Roundcube Webmail :: Welcome to Roundcube Webmail", server=APACHE, login=r"^/(\?_task=login|index\.php)$", action="/?_task=login", home="/?_task=login", accent="#37beff", button="Login", fields=(field("Username", "_user"), field("Password", "_pass", "password")), routes=(("GET", r"^/(\?_task=login)?$", ("login",)),), desc="Webmail login"), http("zimbra", "Zimbra Collaboration", "mail", 8085, "Zimbra Web Client Sign In", server="nginx", login=r"^/(\?loginop=login|zimbra/\?loginop=login)?$", action="/?loginOp=login", home=None, accent="#0d7ec5", button="Sign In", routes=(("GET", r"^/(zimbra/)?$", ("login",)), ("*", r"^/service/", ("status", 401))), desc="Web client login and SOAP; flags CVE-2019-9670 and CVE-2022-27925"), tcp("ssh", "SSH", "remote", 2222, "ssh", desc="OpenSSH banner; records client versions"), tcp("telnet", "Telnet", "remote", 23, "telnet", desc="Login and password prompts; tries count as login attempts"), tcp("ftp", "FTP", "remote", 21, "ftp", desc="vsftpd dialogue; USER/PASS counts as a login attempt"), tcp("rdp", "RDP", "remote", 3389, "rdp", desc="Records RDP negotiation (mstshash cookies, scanners)"), tcp("vnc", "VNC", "remote", 5900, "vnc", desc="RFB 3.8 with VNC authentication challenge"), tcp("smb", "SMB", "remote", 445, "smb", desc="Records SMB negotiation; off by default (Windows uses 445)"), tcp("smtp", "SMTP", "mail", 25, "smtp", desc="Postfix dialogue with AUTH; flags open-relay tests"), tcp("submission", "SMTP submission", "mail", 587, "smtp", desc="Same as SMTP on the submission port"), tcp("pop3", "POP3", "mail", 110, "pop3", desc="Dovecot dialogue; USER/PASS counts as a login attempt"), tcp("imap", "IMAP", "mail", 143, "imap", desc="Dovecot dialogue; LOGIN counts as a login attempt"), tcp("mysql", "MySQL", "db", 3306, "mysql", desc="Sends a MySQL 8 handshake; client auth counts as a login attempt"), tcp("postgres", "PostgreSQL", "db", 5432, "postgres", desc="Asks for a cleartext password; answers 28P01"), tcp("mssql", "Microsoft SQL Server", "db", 1433, "mssql", desc="Records TDS pre-login and login packets"), tcp("redis", "Redis", "db", 6379, "redis", desc="NOAUTH replies; flags CONFIG/SLAVEOF/MODULE abuse"), tcp("mongodb", "MongoDB", "db", 27017, "raw", desc="Records wire-protocol probes"), tcp("memcached", "Memcached", "db", 11211, "memcached", desc="Answers stats/version probes"), tcp("ajp", "Apache JServ (AJP)", "web", 8009, "ajp", desc="Flags Ghostcat (CVE-2020-1938)"), tcp("activemq", "Apache ActiveMQ OpenWire", "devops", 61616, "activemq", desc="Flags CVE-2023-46604 payloads"), tcp("mqtt", "MQTT broker", "iot", 1883, "mqtt", desc="CONNECT with credentials counts as a login attempt"), tcp("modbus", "Modbus/TCP", "iot", 502, "modbus", desc="Records industrial-protocol scans"), tcp("adb", "Android Debug Bridge", "iot", 5555, "adb", desc="Records ADB CNXN attempts (botnet spreaders)"), tcp("winbox", "MikroTik Winbox", "iot", 8291, "raw", desc="Records Winbox protocol probes"), tcp("sip", "SIP (TCP)", "iot", 5060, "sip", desc="REGISTER/INVITE scans; REGISTER with auth counts as a login attempt"), ] BY_KEY = {p["key"]: p for p in PROFILES} BUNDLES = { "classic": ["ftp", "telnet", "telnet:2323", "mssql", "ssh", "mysql", "rdp", "postgres", "vnc", "redis", "admin-panel:8081", "elasticsearch", "memcached", "mongodb"], "recommended": ["exchange", "fortigate", "globalprotect", "netscaler", "ssh", "telnet", "ftp", "rdp", "mysql", "redis", "wordpress", "admin-panel:8081"], "edge": ["fortigate", "globalprotect", "netscaler", "ivanti", "cisco-asa", "f5-bigip", "sonicwall", "checkpoint", "pfsense"], "mail": ["exchange", "smtp", "submission", "imap", "pop3", "roundcube", "zimbra"], "web": ["wordpress", "phpmyadmin", "cpanel", "plesk", "webmin", "tomcat", "weblogic", "admin-panel"], "devops": ["jenkins", "gitlab", "grafana", "kibana", "confluence", "jira", "teamcity", "spring-boot", "docker", "kubernetes", "etcd", "vcenter", "proxmox", "activemq", "activemq-web", "openfire"], "databases": ["mysql", "postgres", "mssql", "redis", "mongodb", "memcached", "elasticsearch", "couchdb"], "remote": ["ssh", "telnet", "ftp", "rdp", "vnc", "winrm"], "iot": ["hikvision", "router", "synology", "mqtt", "modbus", "adb", "winbox", "sip", "telnet:2323"], } def sig(tag, path=None, method=None, header=None, body=None, raw=None, test=None): return {"tag": tag, "path": re.compile(path) if path else None, "method": method, "header": (header[0], re.compile(header[1])) if header else None, "body": re.compile(body) if body else None, "raw": re.compile(raw) if raw else None, "test": test} SIGNATURES = [ sig("cve-2021-44228", test=lambda r: bool(re.search(r"\$\{(jndi|\$\{|lower:|upper:|env:|::-|date:)", r["all"]))), sig("cve-2014-6271", test=lambda r: any(v.startswith("() {") for v in r["headers"].values())), sig("cve-2021-26855", header=("cookie", r"x-(beresource|anonresource-backend)=")), sig("cve-2021-34473", path=r"/autodiscover/autodiscover\.json.*@"), sig("cve-2022-41040", path=r"/autodiscover/autodiscover\.json.*powershell"), sig("cve-2018-13379", path=r"/remote/fgt_lang.*\.\./"), sig("cve-2022-40684", path=r"^/api/v2/cmdb/system/admin", header=("user-agent", r"report runner")), sig("cve-2024-3400", header=("cookie", r"sessid=[^;]*\.\./")), sig("cve-2024-0012", header=("x-pan-authcheck", r"off")), sig("cve-2025-0108", path=r"/unauth/(\.\./|%2e%2e/)"), sig("cve-2019-19781", path=r"/vpns?/(\.\./)?vpns/|/vpns/portal/scripts/"), sig("cve-2023-4966", path=r"^/oauth/idp/\.well-known/openid-configuration", test=lambda r: len(r["host"]) > 1000), sig("cve-2019-11510", path=r"/dana-na/\.\./|guacamole/\.\./"), sig("cve-2023-46805", path=r"/api/v1/totp/user-backup-code/\.\./"), sig("cve-2024-21887", path=r"/api/v1/(license/keys-status|totp/user-backup-code)/.*[;|`]"), sig("cve-2020-3452", path=r"/\+cscot\+/(translation-table|oem-customization).*(\.\./|\+cscoe\+)"), sig("cve-2020-5902", path=r"/tmui/login\.jsp/\.\.;/"), sig("cve-2022-1388", path=r"^/mgmt/tm/util/bash"), sig("cve-2023-46747", path=r"^/tmui/control/form", method="POST"), sig("cve-2024-24919", path=r"^/clients/mycrl", method="POST", body=r"cshell/\.\./"), sig("cve-2023-20198", path=r"^/webui/logoutconfirm\.html\?logon_hash="), sig("cve-2021-21972", path=r"^/ui/vropspluginui/rest/services/uploadova"), sig("cve-2021-21985", path=r"^/ui/h5-vsan/rest/proxy/service/"), sig("cve-2021-22005", path=r"^/analytics/telemetry/ph/api/hyper/send"), sig("cve-2019-15107", path=r"^/password_change\.cgi", method="POST", body=r"(old|expired)=[^&]*\|"), sig("cve-2017-12617", method="PUT", path=r"\.jsp/?$"), sig("cve-2024-23897", path=r"^/cli\b", method="POST"), sig("cve-2021-22205", path=r"^/uploads/user", method="POST"), sig("cve-2021-43798", path=r"^/public/plugins/[^/]+/\.\./"), sig("cve-2019-7609", path=r"^/api/timelion/run", body=r"constructor|prototype"), sig("cve-2021-26084", path=r"(createpage-entervariables|doenterpagevariables)\.action", body=r"queryString|\\u0027|%5cu0027"), sig("cve-2022-26134", path=r"\$\{.*(@java|#|runtime|getruntime)"), sig("cve-2023-22515", path=r"/server-info\.action\?.*bootstrapstatusprovider"), sig("cve-2023-22527", path=r"^/template/aui/text-inline\.vm", method="POST"), sig("cve-2023-42793", path=r"^/app/rest/users/id:\d+/tokens/rpc2"), sig("cve-2024-27198", path=r"\?jsp=.*;\.jsp"), sig("cve-2022-22965", test=lambda r: "class.module.classloader" in r["all"]), sig("cve-2022-22963", header=("spring.cloud.function.routing-expression", r".")), sig("cve-2020-14882", path=r"/console/(css|images)/(\.\./|%2e%2e%2f)"), sig("cve-2023-32315", path=r"/setup/setup-s/(%u002e%u002e|\.\.)/"), sig("cve-2021-41773", raw=r"/(cgi-bin|icons)/(\.%2e|%2e%2e|%%32%65)/"), sig("cve-2024-4577", raw=r"(%ad|-)d\+?allow_url_include"), sig("cve-2017-9841", path=r"phpunit/src/util/php/eval-stdin\.php"), sig("cve-2018-20062", path=r"invokefunction.*call_user_func_array"), sig("cve-2023-34362", path=r"moveitisapi/moveitisapi\.dll"), sig("cve-2021-36260", path=r"^/sdk/weblanguage", method="PUT"), sig("cve-2018-10561", path=r"^/gponform/diag_form"), sig("cve-2023-1389", path=r"/cgi-bin/luci/;stok=/locale"), sig("cve-2017-17215", path=r"^/ctrlt/deviceupgrade_1"), sig("cve-2019-9670", path=r"^/autodiscover/autodiscover\.xml", body=r" 10 else "(set)" if self.key else "(missing)" plan, clashes = self.plan() decoys = ["%s/%s%s" % (port, prof["key"] if prof else "tcp", "+tls" if prof and prof["tls"] else "") for prof, port in plan] lines = [ "version %s" % VERSION, "sensor name %s" % self.name, "api key %s" % masked, "endpoint %s" % self.endpoint, "decoys %s" % (", ".join(decoys) or "-"), "tls cert %s%s" % (self.cert, "" if os.path.exists(self.cert) else " (missing: TLS decoys answer in plain HTTP)"), "ignore %s" % (", ".join(str(n) for n in self.ignore) or "-"), "fail2ban %s" % (self.fail2ban_log or "-"), "log level %s" % self.log_level.lower(), ] if clashes: lines.append("not started %s (port already used by an earlier profile)" % ", ".join("%s:%d" % c for c in clashes)) return "\n".join(lines) class Systemd: def __init__(self): path = os.environ.get("NOTIFY_SOCKET") self.addr = None if path: self.addr = "\0" + path[1:] if path.startswith("@") else path usec = os.environ.get("WATCHDOG_USEC", "") self.watchdog = int(usec) / 2e6 if usec.isdigit() and int(usec) > 0 else None def notify(self, *lines): if not self.addr: return try: with socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) as sock: sock.sendto("\n".join(lines).encode(), self.addr) except OSError as exc: log.debug("sd_notify failed: %s", exc) async def watchdog_loop(self): while self.watchdog: self.notify("WATCHDOG=1") await asyncio.sleep(self.watchdog) # Shared styling for every decoy login page. One themeable stylesheet drives a # handful of layout archetypes (card / stacked / split / console / minimal) so # each profile can look like a distinct, plausible enterprise portal instead of # the same plain card. Accent colours come from the profile; no real vendor logo # or pixel-for-pixel clone is embedded -- a generated wordmark stands in for one. LOGIN_CSS = ( "*{box-sizing:border-box}html,body{margin:0;height:100%}" "body{font:14px/1.5 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;color:#1f2937;" "background:#eef1f5;display:flex;min-height:100vh;align-items:center;justify-content:center;padding:24px}" "a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline}" "h1{font-size:22px;font-weight:600;margin:0 0 4px;color:#111827}" ".sub{margin:0 0 18px;color:#6b7280;font-size:13px}" ".wm{display:inline-flex;align-items:center;gap:10px;font-weight:600;font-size:16px;color:#111827}" ".mark{display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;border-radius:8px;" "background:var(--accent);color:#fff;font-weight:700;font-size:14px;letter-spacing:.5px;flex:0 0 auto}" ".fld{margin:0 0 14px}label{display:block;font-size:13px;margin:0 0 6px;color:#374151;font-weight:500}" "input{width:100%;padding:10px 12px;border:1px solid #d1d5db;border-radius:6px;font:inherit;background:#fff;color:#111827;" "transition:border-color .15s,box-shadow .15s}" "input:focus{outline:0;border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 25%,transparent)}" "button{margin-top:6px;width:100%;padding:11px;border:0;border-radius:6px;background:var(--accent);color:#fff;" "font:inherit;font-weight:600;cursor:pointer;transition:filter .15s}button:hover{filter:brightness(.94)}" ".err{margin:0 0 14px;padding:9px 12px;border-radius:6px;background:#fef2f2;border:1px solid #fecaca;color:#b91c1c;font-size:13px}" ".links{margin:14px 0 0;font-size:13px}.foot{margin:18px 0 0;font-size:12px;color:#9ca3af;text-align:center}" ".card{width:100%;max-width:380px;background:#fff;border-radius:12px;box-shadow:0 10px 30px rgba(16,24,40,.12);padding:32px}" ".card .head{margin:0 0 20px}" ".stacked{width:100%;max-width:400px}.stacked .logo{margin:0 0 22px}" ".stacked .box{background:#fff;border:1px solid #e5e7eb;border-radius:4px;padding:40px;box-shadow:0 2px 6px rgba(0,0,0,.08)}" ".split{display:flex;width:100%;max-width:860px;min-height:460px;background:#fff;border-radius:14px;overflow:hidden;" "box-shadow:0 20px 50px rgba(16,24,40,.18)}" ".split .hero{flex:1;background:linear-gradient(135deg,var(--accent),var(--accent2));color:#fff;display:flex;" "align-items:center;justify-content:center;padding:40px}" ".split .hero .mark{width:48px;height:48px;font-size:19px;background:rgba(255,255,255,.2)}" ".split .heroin{max-width:260px}.split .heroin h2{font-size:24px;margin:16px 0 8px}.split .heroin p{opacity:.85}" ".split .pane{flex:1;padding:48px;display:flex;flex-direction:column;justify-content:center}" "body.scene-console{background:#0b1220}" ".console{width:100%;max-width:380px;background:#121a2b;border:1px solid #1f2a3d;border-radius:10px;overflow:hidden;" "box-shadow:0 18px 40px rgba(0,0,0,.5);color:#e5e7eb}" ".console .bar{padding:18px 28px;background:linear-gradient(90deg,var(--accent),var(--accent2))}" ".console .bar .wm,.console .bar .mark{color:#fff}.console .bar .mark{background:rgba(255,255,255,.22)}" ".console .body{padding:28px}.console h1{color:#f9fafb;font-size:19px}.console .sub{color:#9aa6b8}" ".console label{color:#c3ccda}.console input{background:#0b1220;border-color:#2a3850;color:#e5e7eb}" ".console .foot{color:#5b6b85}" ".minimal{width:100%;max-width:340px;background:#fff;border:1px solid #d9d9d9;border-radius:4px;padding:26px}" "@media(max-width:720px){.split{flex-direction:column;max-width:420px;min-height:0}" ".split .hero{padding:32px}.split .pane{padding:32px}}" ) LAYOUTS = ("card", "stacked", "split", "console", "minimal") def _shade(hexc, factor): """Lighten (factor>0) or darken (factor<0) a #rrggbb colour for gradients.""" try: h = hexc.lstrip("#") r, g, b = int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16) except Exception: r, g, b = 31, 111, 235 if factor >= 0: r, g, b = (int(c + (255 - c) * factor) for c in (r, g, b)) else: r, g, b = (int(c * (1 + factor)) for c in (r, g, b)) return "#%02x%02x%02x" % (r, g, b) def _wordmark(prof): brand = prof.get("brand") or prof["name"] inits = "".join(w[0] for w in re.findall(r"[A-Za-z0-9]+", brand)[:2]).upper() or "S" return '%s%s' % (html.escape(inits), html.escape(brand)) def login_page(prof, action, error=False): layout = prof.get("layout") or "card" if layout not in LAYOUTS: layout = "card" accent = prof["accent"] accent2 = prof.get("accent2") or _shade(accent, -0.22) title = html.escape(prof["title"] or prof["name"]) brand = html.escape(prof.get("brand") or prof["name"]) sub = prof.get("subtitle") or "" subhtml = '

%s

' % html.escape(sub) if sub else "" note = html.escape(prof.get("note") or "") foot = '

%s

' % note if note else "" wm = _wordmark(prof) fields = "".join( '
' '
' % (i, html.escape(label), i, html.escape(name, quote=True), kind, html.escape(label)) for i, (label, name, kind) in enumerate(prof["fields"])) err = '

Invalid credentials. Please try again.

' if error else "" form = ('%s
%s
' % (err, html.escape(action, quote=True), fields, html.escape(prof["button"]))) if layout == "split": body = ('
' '

%s

%s%s%s
' % (wm, brand, html.escape(sub or "Secure access portal"), title, subhtml, form, foot)) elif layout == "stacked": body = ('

%s

%s%s
%s
' % (wm, title, subhtml, form, foot)) elif layout == "console": body = ('
%s

%s

%s%s
%s
' % (wm, title, subhtml, form, foot)) elif layout == "minimal": body = '

%s

%s%s%s
' % (title, subhtml, form, foot) else: body = ('
%s

%s

%s%s%s
' % (wm, title, subhtml, form, foot)) root = ":root{--accent:%s;--accent2:%s}" % (accent, accent2) return ('' '' '%s' '%s' % (title, root, LOGIN_CSS, layout, body)).encode() NOT_FOUND = { "iis": ("text/html", '404 - File or directory not found.' '

404 - File or directory not found.

The resource you are looking for might have been removed.

'), "tomcat": ("text/html;charset=utf-8", 'HTTP Status 404 - Not Found' '

HTTP Status 404 - Not Found


Apache Tomcat/9.0.85

'), "spring": ("application/json", '{"timestamp":"{now}","status":404,"error":"Not Found","path":"{path}"}'), "json": ("application/json", '{"message":"page not found"}'), "apache": ("text/html; charset=iso-8859-1", '404 Not Found' '

Not Found

The requested URL was not found on this server.

'), "nginx": ("text/html", "\r\n404 Not Found\r\n\r\n

404 Not Found

" "\r\n
nginx
\r\n\r\n\r\n"), } REASONS = {200: "OK", 301: "Moved Permanently", 302: "Found", 400: "Bad Request", 401: "Unauthorized", 403: "Forbidden", 404: "Not Found", 405: "Method Not Allowed", 500: "Internal Server Error", 512: "Not Extended"} METHODS = {"GET", "POST", "HEAD", "PUT", "DELETE", "OPTIONS", "PATCH", "PROPFIND", "CONNECT", "TRACE"} def unquote_twice(text): once = urllib.parse.unquote(text) return urllib.parse.unquote(once) if "%" in once else once class Conn: def __init__(self, sensor, reader, writer, ip, port, prof): self.sensor, self.reader, self.writer = sensor, reader, writer self.ip, self.port, self.prof = ip, port, prof self.tags = set() def tag(self, prefix, name=None): tag = tag_of(prefix, name or self.prof["key"]) if tag not in self.tags: self.tags.add(tag) self.sensor.record(self.ip, self.port, tag) async def send(self, data): self.writer.write(data) await self.writer.drain() async def read(self, n=READ_BYTES, timeout=READ_TIMEOUT): return await asyncio.wait_for(self.reader.read(n), timeout) async def exactly(self, n, timeout=READ_TIMEOUT): return await asyncio.wait_for(self.reader.readexactly(n), timeout) async def line(self, timeout=READ_TIMEOUT): data = await asyncio.wait_for(self.reader.readline(), timeout) return data.replace(b"\xff", b"").strip()[:512] async def http_session(c): prof = c.prof for _ in range(4): try: head = await asyncio.wait_for(c.reader.readuntil(b"\r\n\r\n"), READ_TIMEOUT) except (asyncio.IncompleteReadError, asyncio.LimitOverrunError): return text = head.decode("latin-1") lines = text.split("\r\n") parts = lines[0].split(" ") if len(parts) < 3 or parts[0] not in METHODS: c.tag("probe", "non-http") return method, target = parts[0], parts[1][:4096] headers = {} for raw in lines[1:]: if ":" in raw: k, v = raw.split(":", 1) headers[k.strip().lower()] = v.strip()[:8192] body = b"" length = headers.get("content-length", "0") if length.isdigit() and int(length) > 0: try: body = await asyncio.wait_for(c.reader.readexactly(min(int(length), MAX_BODY)), READ_TIMEOUT) except asyncio.IncompleteReadError as exc: body = exc.partial status, extra, payload = respond(c, method, target, headers, body) await asyncio.sleep(random.uniform(0.05, 0.25) if status != 401 else random.uniform(0.3, 0.9)) out = ["HTTP/1.1 %d %s" % (status, REASONS.get(status, "OK")), "Date: " + email.utils.formatdate(usegmt=True)] if prof["server"]: out.append("Server: " + prof["server"]) host = (headers.get("host") or "localhost")[:255] for k, v in prof["headers"] + extra: out.append("%s: %s" % (k, v.replace("{host}", host))) keep = headers.get("connection", "").lower() == "keep-alive" and method != "POST" out.append("Content-Length: %d" % len(payload)) out.append("Connection: " + ("keep-alive" if keep else "close")) await c.send(("\r\n".join(out) + "\r\n\r\n").encode("latin-1", "replace") + (b"" if method == "HEAD" else payload)) if not keep: return def respond(c, method, target, headers, body): prof = c.prof host = re.sub(r"[^A-Za-z0-9.:\-\[\]]", "", headers.get("host") or "localhost")[:255] or "localhost" path_raw = target if target.startswith("/") else "/" + target.split("/", 3)[-1] path = unquote_twice(path_raw).lower() body_text = unquote_twice(body.decode("latin-1")).lower() req = {"method": method, "path": path, "raw": path_raw.lower(), "headers": headers, "host": headers.get("host") or "", "all": " ".join([path, body_text] + [v.lower() for v in headers.values()])} c.tag("http") for s in SIGNATURES: if s["method"] and s["method"] != method: continue if s["path"] and not s["path"].search(path): continue if s["raw"] and not s["raw"].search(req["raw"]): continue if s["header"] and not s["header"][1].search(headers.get(s["header"][0], "").lower()): continue if s["body"] and not s["body"].search(body_text): continue if s["test"] and not s["test"](req): continue c.tag("exploit" if s["tag"][:4] in ("cve-", "dock") else "probe", s["tag"]) def fill(text): return (text.replace("{host}", host).replace("{path}", path_raw[:200].replace('"', "")) .replace("{now}", time.strftime("%Y-%m-%dT%H:%M:%S.000+00:00", time.gmtime()))) if "authorization" in headers and (prof["basic"] or prof["login"]): c.tag("login") route_path = path_raw.split("#")[0].lower() route_path_noq = route_path.split("?")[0] if prof["login"] and method == "POST" and (re.search(prof["login"], route_path_noq) or re.search(prof["login"], route_path)): c.tag("login") if "json" in headers.get("content-type", "") or "api" in route_path_noq: return 401, [("Content-Type", JSON)], b'{"error":"invalid_credentials","message":"Invalid username or password"}' return 200, [("Content-Type", "text/html; charset=utf-8"), ("Cache-Control", "no-store")], login_page(prof, prof["action"], True) for m, pattern, action in prof["routes"]: if m != "*" and m != method and not (m == "GET" and method == "HEAD"): continue if not re.search(pattern, route_path): continue kind = action[0] if kind == "login": return 200, [("Content-Type", "text/html; charset=utf-8"), ("Cache-Control", "no-store")], login_page(prof, prof["action"]) if kind == "redirect": return 302, [("Location", fill(action[1])), ("Content-Type", "text/html")], b"" if kind == "basic": return 401, [("WWW-Authenticate", 'Basic realm="%s"' % fill(action[1])), ("Content-Type", "text/html")], b"401 Unauthorized" if kind == "json": return action[1], [("Content-Type", JSON)], fill(action[2]).encode() if kind == "text": return action[1], [("Content-Type", action[2])], fill(action[3]).encode() if kind == "status": return action[1], [("Content-Type", "text/html")], b"" if route_path_noq in ("/", "") and prof["home"]: return 302, [("Location", fill(prof["home"]))], b"" if route_path_noq in ("/", "") and prof["basic"]: return 401, [("WWW-Authenticate", 'Basic realm="%s"' % prof["basic"]), ("Content-Type", "text/html")], b"401 Unauthorized" if route_path_noq in ("/", "") and prof["login"]: return 200, [("Content-Type", "text/html; charset=utf-8")], login_page(prof, prof["action"]) style = prof["notfound"] or ("iis" if prof["server"].startswith("Microsoft") else "apache" if prof["server"] == APACHE else "nginx") ctype, text = NOT_FOUND[style] return 404, [("Content-Type", ctype)], fill(text).encode() async def d_raw(c): data = await c.read() if data: c.tag("probe", "payload") async def d_ssh(c): await c.send(b"SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.10\r\n") data = await c.read(256) if data.startswith(b"SSH-"): c.tag("client", "ssh") async def d_telnet(c): await c.send(b"\xff\xfb\x01\xff\xfb\x03\xff\xfd\x18\r\nUbuntu 22.04.4 LTS\r\n") for _ in range(3): await c.send(b"login: ") user = await c.line() if not user: return await c.send(b"Password: ") await c.line() c.tag("login") await asyncio.sleep(random.uniform(1.0, 2.0)) await c.send(b"\r\nLogin incorrect\r\n") async def d_ftp(c): await c.send(b"220 (vsFTPd 3.0.5)\r\n") for _ in range(8): cmd = (await c.line()).decode("latin-1").upper() if not cmd: return verb = cmd.split(" ")[0] if verb == "USER": await c.send(b"331 Please specify the password.\r\n") elif verb == "PASS": c.tag("login") await asyncio.sleep(random.uniform(0.8, 1.6)) await c.send(b"530 Login incorrect.\r\n") elif verb in ("AUTH", "PBSZ", "PROT"): await c.send(b"530 Please login with USER and PASS.\r\n") elif verb == "SYST": await c.send(b"215 UNIX Type: L8\r\n") elif verb == "FEAT": await c.send(b"211-Features:\r\n EPRT\r\n EPSV\r\n MDTM\r\n PASV\r\n REST STREAM\r\n SIZE\r\n TVFS\r\n UTF8\r\n211 End\r\n") elif verb == "QUIT": await c.send(b"221 Goodbye.\r\n") return else: await c.send(b"530 Please login with USER and PASS.\r\n") async def d_smtp(c): await c.send(b"220 mail.localdomain ESMTP Postfix (Ubuntu)\r\n") for _ in range(12): line = await c.line() if not line: return cmd = line.decode("latin-1").upper() verb = cmd.split(" ")[0] if verb in ("EHLO", "HELO"): await c.send(b"250-mail.localdomain\r\n250-PIPELINING\r\n250-SIZE 10240000\r\n250-AUTH PLAIN LOGIN\r\n" b"250-ENHANCEDSTATUSCODES\r\n250-8BITMIME\r\n250 SMTPUTF8\r\n" if verb == "EHLO" else b"250 mail.localdomain\r\n") elif verb == "AUTH": mech = cmd.split(" ") if len(mech) > 1 and mech[1] == "LOGIN" and len(mech) == 2: await c.send(b"334 VXNlcm5hbWU6\r\n") await c.line() await c.send(b"334 UGFzc3dvcmQ6\r\n") await c.line() elif len(mech) == 2: await c.send(b"334 \r\n") await c.line() c.tag("login") await asyncio.sleep(random.uniform(0.8, 1.6)) await c.send(b"535 5.7.8 Error: authentication failed: authentication failure\r\n") elif verb == "MAIL": await c.send(b"250 2.1.0 Ok\r\n") elif verb == "RCPT": c.tag("probe", "smtp-relay") await c.send(b"554 5.7.1 : Relay access denied\r\n") elif verb == "STARTTLS": await c.send(b"454 4.7.0 TLS not available due to local problem\r\n") elif verb == "QUIT": await c.send(b"221 2.0.0 Bye\r\n") return elif verb in ("RSET", "NOOP"): await c.send(b"250 2.0.0 Ok\r\n") else: await c.send(b"502 5.5.2 Error: command not recognized\r\n") async def d_pop3(c): await c.send(b"+OK Dovecot (Ubuntu) ready.\r\n") for _ in range(8): cmd = (await c.line()).decode("latin-1").upper() if not cmd: return verb = cmd.split(" ")[0] if verb == "USER": await c.send(b"+OK\r\n") elif verb in ("PASS", "AUTH", "APOP"): c.tag("login") await asyncio.sleep(random.uniform(0.8, 1.6)) await c.send(b"-ERR [AUTH] Authentication failed.\r\n") elif verb == "CAPA": await c.send(b"+OK\r\nCAPA\r\nTOP\r\nUIDL\r\nRESP-CODES\r\nPIPELINING\r\nAUTH-RESP-CODE\r\nUSER\r\nSASL PLAIN LOGIN\r\n.\r\n") elif verb == "QUIT": await c.send(b"+OK Logging out\r\n") return else: await c.send(b"-ERR Unknown command.\r\n") async def d_imap(c): await c.send(b"* OK [CAPABILITY IMAP4rev1 SASL-IR LOGIN-REFERRALS ID ENABLE IDLE LITERAL+ AUTH=PLAIN AUTH=LOGIN] Dovecot (Ubuntu) ready.\r\n") for _ in range(8): line = (await c.line()).decode("latin-1") if not line: return bits = line.split(" ") tagid, verb = bits[0][:16], (bits[1].upper() if len(bits) > 1 else "") if verb in ("LOGIN", "AUTHENTICATE"): c.tag("login") await asyncio.sleep(random.uniform(0.8, 1.6)) await c.send(("%s NO [AUTHENTICATIONFAILED] Authentication failed.\r\n" % tagid).encode()) elif verb == "CAPABILITY": await c.send(("* CAPABILITY IMAP4rev1 SASL-IR LOGIN-REFERRALS ID ENABLE IDLE LITERAL+ AUTH=PLAIN AUTH=LOGIN\r\n%s OK Pre-login capabilities listed.\r\n" % tagid).encode()) elif verb == "LOGOUT": await c.send(("* BYE Logging out\r\n%s OK Logout completed.\r\n" % tagid).encode()) return else: await c.send(("%s BAD Error in IMAP command received by server.\r\n" % tagid).encode()) def mysql_handshake(): salt = bytes(random.randint(33, 126) for _ in range(20)) body = (b"\x0a" + b"8.0.36-0ubuntu0.22.04.1\x00" + struct.pack(" 36: c.tag("login") await asyncio.sleep(random.uniform(0.3, 0.8)) msg = b"Access denied for user" err = b"\xff" + struct.pack(" 8: await c.exactly(min(length - 8, 4096)) break await c.send(b"R" + struct.pack("!II", 8, 3)) kind = await c.exactly(1) if kind == b"p": c.tag("login") await asyncio.sleep(random.uniform(0.5, 1.0)) fields = b"SFATAL\x00VFATAL\x00C28P01\x00Mpassword authentication failed\x00\x00" await c.send(b"E" + struct.pack("!I", len(fields) + 4) + fields) async def d_mssql(c): data = await c.read() if data[:1] == b"\x12": c.tag("client", "mssql-prelogin") reply = bytes.fromhex("0401002500000100000015000601001b000102001c000103001d0000ff0e0007d00000000000") await c.send(reply) data = await c.read() if data[:1] == b"\x10": c.tag("login") async def d_redis(c): for _ in range(10): data = await c.read() if not data: return text = data.decode("latin-1").upper() if re.search(r"\b(CONFIG|SLAVEOF|REPLICAOF|MODULE|EVAL|SCRIPT)\b", text): c.tag("exploit", "redis-abuse") if "AUTH" in text: c.tag("login") await c.send(b"-WRONGPASS invalid username-password pair or user is disabled.\r\n") elif "PING" in text and "AUTH" not in text: await c.send(b"-NOAUTH Authentication required.\r\n") else: await c.send(b"-NOAUTH Authentication required.\r\n") async def d_memcached(c): for _ in range(5): line = (await c.line()).decode("latin-1").lower() if not line: return if line.startswith("version"): await c.send(b"VERSION 1.6.14\r\n") elif line.startswith("stats"): await c.send(b"STAT pid 1\r\nSTAT uptime 864211\r\nSTAT version 1.6.14\r\nEND\r\n") else: await c.send(b"ERROR\r\n") async def d_vnc(c): await c.send(b"RFB 003.008\n") version = await c.exactly(12) if not version.startswith(b"RFB"): return await c.send(b"\x01\x02") await c.exactly(1) await c.send(os.urandom(16)) await c.exactly(16) c.tag("login") reason = b"Authentication failed" await c.send(struct.pack("!I", 1) + struct.pack("!I", len(reason)) + reason) async def d_rdp(c): data = await c.read() if data[:1] == b"\x03": c.tag("client", "rdp") if b"mstshash=" in data: c.tag("probe", "rdp-cookie") await c.send(bytes.fromhex("030000130ed000001234000209080002000000")) await c.read() async def d_smb(c): data = await c.read() if b"\xffSMB" in data: c.tag("client", "smb1") elif b"\xfeSMB" in data: c.tag("client", "smb2") async def d_ajp(c): data = await c.read(4096) if data[:2] == b"\x12\x34": c.tag("client", "ajp") if b"javax.servlet.include" in data: c.tag("exploit", "cve-2020-1938") async def d_activemq(c): data = await c.read(4096) if data: c.tag("client", "openwire") if b"ClassPathXmlApplicationContext" in data or b"FileSystemXmlApplicationContext" in data: c.tag("exploit", "cve-2023-46604") async def d_mqtt(c): data = await c.read() if data[:1] == b"\x10": flags_at = data.find(b"MQTT") if flags_at > 0 and len(data) > flags_at + 5 and data[flags_at + 5] & 0x80: c.tag("login") else: c.tag("client", "mqtt") await c.send(b"\x20\x02\x00\x05") async def d_modbus(c): data = await c.read() if len(data) >= 8 and data[2:4] == b"\x00\x00": c.tag("client", "modbus") await c.send(data[:7] + bytes([data[7] | 0x80, 0x01])) async def d_adb(c): data = await c.read() if data.startswith(b"CNXN"): c.tag("client", "adb") if b"shell:" in data or b"busybox" in data: c.tag("exploit", "adb-shell") async def d_sip(c): data = (await c.read(4096)).decode("latin-1") first = data.split(" ", 1)[0].upper() if first in ("REGISTER", "INVITE", "OPTIONS"): c.tag("client", "sip-" + first.lower()) if first == "REGISTER" and "authorization:" in data.lower(): c.tag("login") call_id = re.search(r"(?im)^call-id:\s*(.+)$", data) reply = "SIP/2.0 401 Unauthorized\r\nCall-ID: %s\r\nWWW-Authenticate: Digest realm=\"asterisk\",nonce=\"%08x\"\r\nContent-Length: 0\r\n\r\n" % ( (call_id.group(1).strip()[:80] if call_id else "0"), random.getrandbits(32)) await c.send(reply.encode()) DIALOGUES = {"raw": d_raw, "ssh": d_ssh, "telnet": d_telnet, "ftp": d_ftp, "smtp": d_smtp, "pop3": d_pop3, "imap": d_imap, "mysql": d_mysql, "postgres": d_postgres, "mssql": d_mssql, "redis": d_redis, "memcached": d_memcached, "vnc": d_vnc, "rdp": d_rdp, "smb": d_smb, "ajp": d_ajp, "activemq": d_activemq, "mqtt": d_mqtt, "modbus": d_modbus, "adb": d_adb, "sip": d_sip} class Sensor: def __init__(self, config): self.cfg = config self.pending = {} self.own = set() self.slots = None self.per_source = {} self.servers = [] self.listening = [] self.by_port = {} self.active = set() self.systemd = Systemd() self.disabled = False self.stats = {"connections": 0, "sources": set(), "reported": 0, "reports": 0, "failures": 0, "tags": {}} self.started = time.time() self.retry_after = 0 @staticmethod def normalise(ip): try: addr = ipaddress.ip_address(ip.split("%", 1)[0]) except ValueError: return None mapped = getattr(addr, "ipv4_mapped", None) return mapped or addr def reportable(self, addr): if addr is None or not addr.is_global or str(addr) in self.own: return False return not any(addr in net for net in self.cfg.ignore) def record(self, ip, port, proto): addr = self.normalise(ip) if not self.reportable(addr): return ip = str(addr) now = int(time.time()) key = (ip, port, proto) entry = self.pending.get(key) if entry: entry[1] = now entry[2] += 1 elif len(self.pending) < MAX_PENDING: self.pending[key] = [now, now, 1] level = logging.WARNING if proto.startswith("exploit:") else logging.INFO log.log(level, "%s -> %d/%s", ip, port, proto) self.stats["sources"].add(ip) kind = proto.split(":", 1)[0] self.stats["tags"][kind] = self.stats["tags"].get(kind, 0) + 1 def tls_context(self): if not os.path.exists(self.cfg.cert) or not os.path.exists(self.cfg.cert_key): return None try: ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) ctx.options |= getattr(ssl, "OP_NO_COMPRESSION", 0) ctx.load_cert_chain(self.cfg.cert, self.cfg.cert_key) return ctx except (OSError, ssl.SSLError) as exc: log.warning("cannot load TLS certificate (%s); TLS decoys answer in plain HTTP", exc) return None def handler(self, prof, port): proto = prof["key"] if prof else "tcp" async def handle(reader, writer): task = current_task() if task is not None: self.active.add(task) task.add_done_callback(self.active.discard) peer = writer.get_extra_info("peername") or ("", 0) ip = peer[0] self.stats["connections"] += 1 self.record(ip, port, proto) if self.slots is None or self.slots.locked() or self.per_source.get(ip, 0) >= MAX_PER_SOURCE: close(writer) return self.per_source[ip] = self.per_source.get(ip, 0) + 1 conn = Conn(self, reader, writer, ip, port, prof or {"key": "tcp"}) try: async with self.slots: if prof is None: await asyncio.wait_for(d_raw(conn), SESSION_SECONDS) elif prof["kind"] == "http": await asyncio.wait_for(http_session(conn), SESSION_SECONDS) else: await asyncio.wait_for(DIALOGUES[prof["dialogue"]](conn), SESSION_SECONDS) except Exception as exc: log.debug("%s:%d session ended: %r", ip, port, exc) finally: left = self.per_source.get(ip, 1) - 1 if left > 0: self.per_source[ip] = left else: self.per_source.pop(ip, None) close(writer) return handle def exception_handler(self, loop, context): exc = context.get("exception") transport = context.get("transport") if isinstance(exc, (ssl.SSLError, ConnectionError, OSError)) and transport is not None: try: peer = transport.get_extra_info("peername") local = transport.get_extra_info("sockname") prof = self.by_port.get(local[1]) if local else None if peer and prof and prof["tls"]: self.record(peer[0], local[1], tag_of("tls", prof["key"])) except Exception: pass return loop.default_exception_handler(context) async def listen(self): plan, clashes = self.cfg.plan() for key, port in clashes: log.warning("profile %s not started: port %d is already used by another profile", key, port) tls = self.tls_context() if any(p and p["tls"] for p, _ in plan) else None if any(p and p["tls"] for p, _ in plan) and tls is None: log.warning("no TLS certificate at %s; TLS decoys answer in plain HTTP", self.cfg.cert) for prof, port in plan: started = False context = tls if prof and prof["tls"] else None for host in ("0.0.0.0", "::"): try: server = await asyncio.start_server(self.handler(prof, port), host=host, port=port, ssl=context, reuse_address=True, backlog=128, limit=MAX_HEADER) self.servers.append(server) started = True except OSError: continue label = prof["key"] if prof else "tcp" if started: self.listening.append(port) if prof: self.by_port[port] = prof log.info("listening on port %d (%s%s)", port, label, ", tls" if context else "") else: log.info("port %d is in use or not permitted - %s skipped", port, label) if not self.servers and not self.cfg.fail2ban_log: log.error("no port could be opened and no fail2ban log is set - nothing to do") raise SystemExit(2) def learn_own_addresses(self): try: for info in socket.getaddrinfo(socket.gethostname(), None): self.own.add(str(self.normalise(info[4][0]))) except OSError: pass for family, target in ((socket.AF_INET, "192.0.2.1"), (socket.AF_INET6, "2001:db8::1")): try: with socket.socket(family, socket.SOCK_DGRAM) as sock: sock.connect((target, 9)) self.own.add(str(self.normalise(sock.getsockname()[0]))) except OSError: pass self.own.discard("None") async def follow_fail2ban(self): pattern = re.compile(r"\[([\w.-]+)\]\s+Ban\s+([0-9A-Fa-f:.]+)") path, position, warned = self.cfg.fail2ban_log, None, False while True: try: size = os.path.getsize(path) if position is None or size < position: position = size if size > position: with open(path, "r", errors="replace") as fh: fh.seek(position) for line in fh: m = pattern.search(line) if m: self.record(m.group(2), 0, "fail2ban:" + re.sub(r"[^a-z0-9._-]", "-", m.group(1).lower())[:23]) position = fh.tell() warned = False except OSError as exc: if not warned: log.warning("cannot read %s: %s", path, exc) warned = True await asyncio.sleep(5) def post(self, events): body = json.dumps({"sensor": self.cfg.name, "version": VERSION, "events": events, "profiles": sorted({p["key"] for p in self.by_port.values()})}).encode() req = urllib.request.Request(self.cfg.endpoint, data=body, method="POST", headers={ "Key": self.cfg.key, "Content-Type": "application/json", "User-Agent": USER_AGENT}) with urllib.request.urlopen(req, timeout=30) as resp: return json.loads(resp.read().decode("utf-8") or "{}") async def flush_once(self): items = list(self.pending.items())[:MAX_BATCH] events = [{"ip": ip, "port": port, "proto": proto, "first": f, "last": l, "count": c} for (ip, port, proto), (f, l, c) in items] def drop(): for k, _ in items: self.pending.pop(k, None) try: result = await asyncio.get_event_loop().run_in_executor(None, self.post, events) except urllib.error.HTTPError as exc: detail = exc.read()[:200].decode(errors="replace").strip() self.stats["failures"] += 1 if exc.code == 429 or exc.code >= 500: retry = exc.headers.get("Retry-After", "") log.warning("report deferred (HTTP %s), will retry", exc.code) return int(retry) if retry.isdigit() else 0 if exc.code == 403 and "disabled" in detail.lower(): if not self.disabled: log.error("this sensor is paused in your IP-DB dashboard; events are discarded until it is resumed") self.disabled = True else: log.error("report rejected (HTTP %s): %s", exc.code, detail or "-") drop() return MAX_BACKOFF if self.disabled else 0 except Exception as exc: self.stats["failures"] += 1 log.warning("report failed (%s), will retry", exc) return 0 drop() if self.disabled: log.info("sensor resumed in the dashboard; reporting again") self.disabled = False self.stats["reported"] += len(events) self.stats["reports"] += 1 log.info("reported %d events (accepted %s)", len(events), result.get("accepted", "?")) return None async def flush(self): for _ in range(POSTS_PER_FLUSH): if not self.pending: return True wait = await self.flush_once() if wait is not None: self.retry_after = wait return False return not self.pending async def report_loop(self): delay = self.cfg.flush_seconds while True: await asyncio.sleep(delay) self.retry_after = 0 if await self.flush(): delay = self.cfg.flush_seconds else: delay = max(self.retry_after, min(delay * 2, MAX_BACKOFF)) * random.uniform(0.9, 1.1) self.update_status() def update_status(self): self.systemd.notify("STATUS=%d decoys, %d pending, %d events reported" % (len(self.listening), len(self.pending), self.stats["reported"])) async def stats_loop(self): while True: await asyncio.sleep(STATS_SECONDS) s = self.stats kinds = ", ".join("%s %d" % kv for kv in sorted(s["tags"].items())) or "-" log.info("last hour: %d connections from %d addresses (%s); %d events in %d reports, %d failed", s["connections"], len(s["sources"]), kinds, s["reported"], s["reports"], s["failures"]) self.stats = {"connections": 0, "sources": set(), "reported": 0, "reports": 0, "failures": 0, "tags": {}} async def run(self): loop = asyncio.get_event_loop() loop.set_exception_handler(self.exception_handler) self.slots = asyncio.Semaphore(MAX_CONCURRENT) self.learn_own_addresses() await self.listen() tasks = [asyncio.ensure_future(self.report_loop()), asyncio.ensure_future(self.stats_loop())] if self.systemd.watchdog: tasks.append(asyncio.ensure_future(self.systemd.watchdog_loop())) if self.cfg.fail2ban_log: log.info("following %s", self.cfg.fail2ban_log) tasks.append(asyncio.ensure_future(self.follow_fail2ban())) stop = loop.create_future() for sig_ in (signal.SIGTERM, signal.SIGINT): try: loop.add_signal_handler(sig_, lambda: stop.done() or stop.set_result(True)) except NotImplementedError: pass self.systemd.notify("READY=1") self.update_status() log.info("ready: %d decoys open, reporting every %ds", len(self.listening), self.cfg.flush_seconds) await stop self.systemd.notify("STOPPING=1") log.info("stopping - sending what is left") for server in self.servers: server.close() for task in list(self.active): task.cancel() if self.active: await asyncio.wait(list(self.active), timeout=2) await self.flush() for task in tasks: task.cancel() def current_task(): getter = getattr(asyncio, "current_task", None) or getattr(asyncio.Task, "current_task", None) try: return getter() if getter else None except RuntimeError: return None def close(writer): try: writer.close() except Exception: pass def check(cfg): try: result = Sensor(cfg).post([]) except urllib.error.HTTPError as exc: print("FAILED: HTTP %s %s" % (exc.code, exc.read()[:200].decode(errors="replace"))) return 1 except Exception as exc: print("FAILED: %s" % exc) return 1 print("OK: IP-DB accepted the key for sensor '%s'" % result.get("sensor", cfg.name)) return 0 def catalog(): return { "version": VERSION, "categories": [{"key": k, "name": n} for k, n in CATEGORIES], "profiles": [{"key": p["key"], "name": p["name"], "category": p["cat"], "port": p["port"], "kind": p["kind"], "tls": p["tls"], "description": p["desc"]} for p in PROFILES], "bundles": BUNDLES, "signatures": sorted({s["tag"] for s in SIGNATURES}), } def list_profiles(as_json): if as_json: print(json.dumps(catalog(), indent=1)) return 0 for cat, title in CATEGORIES: print("\n%s" % title) for p in PROFILES: if p["cat"] == cat: print(" %-14s %5d%-5s %s" % (p["key"], p["port"], "/tls" if p["tls"] else "", p["name"])) print("\nBundles: " + " ".join("@%s" % b for b in BUNDLES)) print("Example: IPDB_PROFILES=@edge,exchange:443,ssh") return 0 def main(argv=None): parser = argparse.ArgumentParser( prog="ipdb-sensor", description="IP-DB Sensor - a multi-profile honeypot that reports to IP-DB.", epilog="Settings come from the environment; see the top of this file.") parser.add_argument("--check", action="store_true", help="confirm the API key works; sends no events") parser.add_argument("--config", action="store_true", help="print the effective configuration and exit") parser.add_argument("--list-profiles", action="store_true", help="list decoy profiles and bundles") parser.add_argument("--json", action="store_true", help="with --list-profiles: machine-readable output") parser.add_argument("--version", action="version", version=VERSION) args = parser.parse_args(argv) if args.list_profiles: return list_profiles(args.json) fmt = "%(levelname)-7s %(message)s" if os.environ.get("JOURNAL_STREAM") else "%(asctime)s %(levelname)-7s %(message)s" logging.basicConfig(level=logging.INFO, format=fmt, datefmt="%H:%M:%S", stream=sys.stdout) try: cfg = Config(os.environ) if args.config: print(cfg.describe()) return 0 cfg.validate() except ConfigError as exc: print("ipdb-sensor: %s" % exc, file=sys.stderr) return 2 logging.getLogger().setLevel(cfg.log_level) if args.check: return check(cfg) log.info("IP-DB Sensor %s starting as '%s'", VERSION, cfg.name) loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) try: loop.run_until_complete(Sensor(cfg).run()) finally: loop.close() return 0 if __name__ == "__main__": sys.exit(main())