Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 198 additions & 0 deletions backend/app/analysis/checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@
check_mqtt_open — port 1883 open (no TLS) medium
check_open_dns_resolver — port 53 open medium
check_modbus_open — port 502 open high
check_snmp_exposed — port 161/udp open high
check_redis_exposed — port 6379 open critical
check_docker_daemon_tcp — port 2375 open (no TLS) critical
check_docker_daemon_tls — port 2376 open (TLS) high
check_elasticsearch_open — port 9200 open high
check_portainer_exposed — ports 9000/9443 open medium
check_home_assistant_exposed — port 8123 open medium
check_tftp_open — port 69/udp open medium
check_wireguard_vpn — port 51820/udp open low
"""

from __future__ import annotations
Expand Down Expand Up @@ -419,6 +428,186 @@ def check_modbus_open(device: Device) -> list[RiskData]:
]


def check_snmp_exposed(device: Device) -> list[RiskData]:
"""HIGH — SNMP (port 161/udp) open; default community string leaks device info."""
ports = _has_port(device, 161, protocol="udp")
if not ports:
return []
return [
RiskData(
check_id="snmp_exposed",
severity="high",
title="SNMP port open",
description=(
f"Device {device.ip_address} has SNMP (port 161/udp) open. "
"Devices using the default 'public' community string expose full device "
"information to anyone on the network. Change the community string or "
"switch to SNMPv3 with authentication and encryption."
),
)
]


def check_redis_exposed(device: Device) -> list[RiskData]:
"""CRITICAL — Redis (port 6379) open; no authentication by default."""
ports = _has_port(device, 6379)
if not ports:
return []
return [
RiskData(
check_id="redis_exposed",
severity="critical",
title="Redis database port open",
description=(
f"Device {device.ip_address} has Redis (port 6379/tcp) open. "
"Redis has no authentication enabled by default, and the CONFIG SET "
"command can be used to write arbitrary files and gain remote code "
"execution. Bind Redis to 127.0.0.1 and require a strong password."
),
)
]


def check_docker_daemon_tcp(device: Device) -> list[RiskData]:
"""CRITICAL — Docker TCP daemon (port 2375) open; unauthenticated API."""
ports = _has_port(device, 2375)
if not ports:
return []
return [
RiskData(
check_id="docker_daemon_tcp",
severity="critical",
title="Docker daemon exposed without TLS",
description=(
f"Device {device.ip_address} has the Docker daemon API (port 2375/tcp) "
"open without TLS. This allows anyone on the network to run containers, "
"mount the host filesystem, and gain full root access to the host. "
"Disable the TCP listener or enable TLS client authentication immediately."
),
)
]


def check_docker_daemon_tls(device: Device) -> list[RiskData]:
"""HIGH — Docker TLS daemon (port 2376) open; verify TLS is properly configured."""
ports = _has_port(device, 2376)
if not ports:
return []
return [
RiskData(
check_id="docker_daemon_tls",
severity="high",
title="Docker daemon TLS port open",
description=(
f"Device {device.ip_address} has the Docker daemon TLS port (2376/tcp) "
"open. While TLS is required on this port, it still exposes the Docker "
"API remotely. Verify that client certificate authentication is enforced "
"and restrict access to trusted hosts only."
),
)
]


def check_elasticsearch_open(device: Device) -> list[RiskData]:
"""HIGH — Elasticsearch HTTP (port 9200) open; historically unauthenticated."""
ports = _has_port(device, 9200)
if not ports:
return []
return [
RiskData(
check_id="elasticsearch_open",
severity="high",
title="Elasticsearch HTTP port open",
description=(
f"Device {device.ip_address} has Elasticsearch (port 9200/tcp) open. "
"Older versions of Elasticsearch had no authentication, leading to "
"widespread data breaches. Ensure X-Pack security is enabled, require "
"authentication, and restrict network access to trusted hosts."
),
)
]


def check_portainer_exposed(device: Device) -> list[RiskData]:
"""MEDIUM — Portainer admin UI (ports 9000/9443) open."""
ports = _has_port(device, 9000) + _has_port(device, 9443)
if not ports:
return []
port_list = ", ".join(str(p.port_number) for p in ports)
return [
RiskData(
check_id="portainer_exposed",
severity="medium",
title="Portainer Docker management UI exposed",
description=(
f"Device {device.ip_address} has Portainer (port(s) {port_list}) open. "
"Portainer provides a web UI for managing the entire Docker stack. "
"Ensure a strong admin password is set, enable HTTPS only, and restrict "
"access to trusted hosts with a firewall rule."
),
)
]


def check_home_assistant_exposed(device: Device) -> list[RiskData]:
"""MEDIUM — Home Assistant (port 8123) open on the LAN."""
ports = _has_port(device, 8123)
if not ports:
return []
return [
RiskData(
check_id="home_assistant_exposed",
severity="medium",
title="Home Assistant web interface exposed",
description=(
f"Device {device.ip_address} has Home Assistant (port 8123/tcp) open. "
"Home Assistant controls smart home devices and may have access to "
"locks, cameras, and alarms. Ensure a strong password is set, enable "
"two-factor authentication, and avoid exposing this port to the internet."
),
)
]


def check_tftp_open(device: Device) -> list[RiskData]:
"""MEDIUM — TFTP (port 69/udp) open; no authentication protocol."""
ports = _has_port(device, 69, protocol="udp")
if not ports:
return []
return [
RiskData(
check_id="tftp_open",
severity="medium",
title="TFTP port open",
description=(
f"Device {device.ip_address} has TFTP (port 69/udp) open. "
"TFTP (Trivial File Transfer Protocol) has no authentication mechanism "
"and transfers files in plaintext. It is commonly used for network "
"booting and router firmware — disable it if not actively needed."
),
)
]


def check_wireguard_vpn(device: Device) -> list[RiskData]:
"""LOW — WireGuard VPN (port 51820/udp) detected; informational."""
ports = _has_port(device, 51820, protocol="udp")
if not ports:
return []
return [
RiskData(
check_id="wireguard_vpn",
severity="low",
title="WireGuard VPN port open",
description=(
f"Device {device.ip_address} has WireGuard VPN (port 51820/udp) open. "
"This is informational — WireGuard is a modern, secure VPN protocol. "
"Ensure only authorised peers are configured and keep WireGuard updated."
),
)
]


# ── Master list of all checks ─────────────────────────────────────────────────

ALL_CHECKS = [
Expand All @@ -435,4 +624,13 @@ def check_modbus_open(device: Device) -> list[RiskData]:
check_mqtt_open,
check_open_dns_resolver,
check_modbus_open,
check_snmp_exposed,
check_redis_exposed,
check_docker_daemon_tcp,
check_docker_daemon_tls,
check_elasticsearch_open,
check_portainer_exposed,
check_home_assistant_exposed,
check_tftp_open,
check_wireguard_vpn,
]
149 changes: 149 additions & 0 deletions backend/app/recommendations/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,155 @@ class _Advice:
effort="medium",
impact="high",
),
"snmp_exposed": _Advice(
title="Secure or disable SNMP",
description=(
"SNMP with the default 'public' community string leaks full device information "
"to anyone on the network. Upgrade to SNMPv3 or disable SNMP entirely."
),
steps=[
"Log in to the device management interface.",
"Locate the SNMP configuration section.",
"If SNMP is not required, disable it completely.",
"If SNMP is needed, change the community string from 'public' "
"to a strong random value.",
"Upgrade to SNMPv3 with AuthPriv mode (authentication + encryption) where supported.",
"Restrict SNMP access to specific management hosts using an ACL.",
],
effort="low",
impact="high",
),
"redis_exposed": _Advice(
title="Bind Redis to localhost and require authentication",
description=(
"An unauthenticated Redis instance accessible from the network is a critical "
"vulnerability enabling data theft and remote code execution via CONFIG commands."
),
steps=[
"Edit redis.conf and set 'bind 127.0.0.1' to restrict listening to localhost only.",
"Set a strong password: 'requirepass <strong-password>'.",
"If Redis must be network-accessible, use TLS (Redis 6+) and require authentication.",
"Apply a firewall rule to block port 6379 from all but authorised hosts.",
"Restart Redis and verify the configuration with 'redis-cli ping' from a remote host.",
],
effort="low",
impact="critical",
),
"docker_daemon_tcp": _Advice(
title="Disable the unauthenticated Docker TCP socket",
description=(
"The Docker TCP daemon without TLS allows anyone to run containers, mount the "
"host filesystem, and gain root access. This must be remediated immediately."
),
steps=[
"Edit /etc/docker/daemon.json and remove or comment out the 'hosts' entry for tcp://.",
"If remote Docker access is required, configure TLS with client "
"certificates instead "
"(dockerd --tlsverify --tlscacert=ca.pem "
"--tlscert=server-cert.pem --tlskey=server-key.pem).",
"Restart Docker: 'sudo systemctl restart docker'.",
"Apply a firewall rule to block port 2375 immediately as a temporary measure.",
"Verify the TCP socket is no longer listening with 'ss -tlnp | grep 2375'.",
],
effort="low",
impact="critical",
),
"docker_daemon_tls": _Advice(
title="Restrict Docker TLS daemon access",
description=(
"The Docker TLS daemon port exposes the full Docker API remotely. "
"Verify that mutual TLS client authentication is enforced and access is restricted."
),
steps=[
"Confirm TLS is configured with --tlsverify and a CA, server cert, and server key.",
"Ensure --tlsverify is set so only clients with a valid certificate can connect.",
"Apply a firewall rule to allow port 2376 only from trusted management hosts.",
"Rotate certificates periodically and revoke access for unused clients.",
],
effort="medium",
impact="high",
),
"elasticsearch_open": _Advice(
title="Enable Elasticsearch authentication and restrict network access",
description=(
"Elasticsearch without authentication has led to numerous data breaches. "
"Enable X-Pack security and restrict network access."
),
steps=[
"Enable X-Pack security in elasticsearch.yml: 'xpack.security.enabled: true'.",
"Set passwords for built-in users: 'bin/elasticsearch-setup-passwords interactive'.",
"Configure TLS for inter-node and client communication.",
"Apply a firewall rule to allow port 9200 only from authorised application hosts.",
"Review cluster settings to confirm no anonymous access is permitted.",
],
effort="medium",
impact="high",
),
"portainer_exposed": _Advice(
title="Secure Portainer and restrict network access",
description=(
"Portainer controls your entire Docker environment. A compromised Portainer "
"instance gives full access to all running containers and the host."
),
steps=[
"Set a strong admin password if not already done (Portainer will prompt on first run).",
"Enable HTTPS-only access — disable port 9000 (HTTP) and use 9443 (HTTPS) only.",
"Apply a firewall rule to allow Portainer ports only from trusted management hosts.",
"Consider placing Portainer behind a VPN or SSH tunnel.",
"Enable two-factor authentication in Portainer settings if available.",
],
effort="low",
impact="medium",
),
"home_assistant_exposed": _Advice(
title="Secure Home Assistant and enable 2FA",
description=(
"Home Assistant controls smart home devices. Ensure it is properly secured "
"against unauthorised access from the local network."
),
steps=[
"Enable two-factor authentication (TOTP) in your Home Assistant profile settings.",
"Set a strong password for all user accounts.",
"Apply a firewall rule or network policy to restrict port 8123 to trusted devices.",
"If remote access is needed, use the Nabu Casa cloud service or a VPN rather than "
"direct port forwarding.",
"Keep Home Assistant updated to receive security patches.",
],
effort="low",
impact="medium",
),
"tftp_open": _Advice(
title="Disable TFTP if not actively required",
description=(
"TFTP has no authentication and transfers files in plaintext. "
"Disable it unless it is actively used for network booting or firmware updates."
),
steps=[
"Identify the service using TFTP (port 69/udp) — "
"common culprits: tftpd, dnsmasq, routers.",
"If TFTP is not required, disable or stop the service.",
"If TFTP is needed (e.g., PXE booting), restrict access to specific client IPs.",
"Apply a firewall rule to block port 69/udp from untrusted network segments.",
],
effort="low",
impact="medium",
),
"wireguard_vpn": _Advice(
title="Review WireGuard peer configuration",
description=(
"WireGuard is a secure VPN protocol. This entry is informational — "
"verify the peer list and keep WireGuard updated."
),
steps=[
"Review the WireGuard configuration (/etc/wireguard/*.conf) for authorised peers only.",
"Remove any stale or unused peer entries.",
"Ensure the WireGuard package is kept up to date.",
"Consider using a non-default port if the VPN endpoint "
"should not be easily discoverable.",
],
effort="low",
impact="low",
),
}


Expand Down
Loading
Loading