remove infra.md.example, infra.md is the source of truth

This commit is contained in:
Azreen Jamal
2026-03-03 03:06:13 +08:00
parent 1ad3033cc1
commit a3c6d09350
86 changed files with 17093 additions and 39 deletions

View File

@@ -0,0 +1,328 @@
"""Network scanner for AYN Antivirus.
Inspects active TCP/UDP connections for traffic to known mining pools,
suspicious ports, and unexpected listening services. Also audits
``/etc/resolv.conf`` for DNS hijacking indicators.
"""
from __future__ import annotations
import logging
import re
from pathlib import Path
from typing import Any, Dict, List, Optional
import psutil
from ayn_antivirus.constants import (
CRYPTO_POOL_DOMAINS,
SUSPICIOUS_PORTS,
)
from ayn_antivirus.scanners.base import BaseScanner
logger = logging.getLogger(__name__)
# Well-known system services that are *expected* to listen — extend as needed.
_EXPECTED_LISTENERS = {
22: "sshd",
53: "systemd-resolved",
80: "nginx",
443: "nginx",
3306: "mysqld",
5432: "postgres",
6379: "redis-server",
8080: "java",
}
# Known-malicious / suspicious public DNS servers sometimes injected by
# malware into resolv.conf to redirect DNS queries.
_SUSPICIOUS_DNS_SERVERS = [
"8.8.4.4", # not inherently bad, but worth noting if unexpected
"1.0.0.1",
"208.67.222.123",
"198.54.117.10",
"77.88.8.7",
"94.140.14.14",
]
class NetworkScanner(BaseScanner):
"""Scan active network connections for suspicious activity.
Wraps :func:`psutil.net_connections` and enriches each connection with
process ownership and threat classification.
"""
# ------------------------------------------------------------------
# BaseScanner interface
# ------------------------------------------------------------------
@property
def name(self) -> str:
return "network_scanner"
@property
def description(self) -> str:
return "Inspects network connections for mining pools and suspicious ports"
def scan(self, target: Any = None) -> Dict[str, Any]:
"""Run a full network scan.
*target* is ignored — all connections are inspected.
Returns
-------
dict
``total``, ``suspicious``, ``unexpected_listeners``, ``dns_issues``.
"""
all_conns = self.get_all_connections()
suspicious = self.find_suspicious_connections()
listeners = self.check_listening_ports()
dns = self.check_dns_queries()
return {
"total": len(all_conns),
"suspicious": suspicious,
"unexpected_listeners": listeners,
"dns_issues": dns,
}
# ------------------------------------------------------------------
# Connection enumeration
# ------------------------------------------------------------------
@staticmethod
def get_all_connections() -> List[Dict[str, Any]]:
"""Return a snapshot of every inet connection.
Each dict contains: ``fd``, ``family``, ``type``, ``local_addr``,
``remote_addr``, ``status``, ``pid``, ``process_name``.
"""
result: List[Dict[str, Any]] = []
try:
connections = psutil.net_connections(kind="inet")
except psutil.AccessDenied:
logger.warning("Insufficient permissions to read network connections")
return result
for conn in connections:
local = f"{conn.laddr.ip}:{conn.laddr.port}" if conn.laddr else ""
remote = f"{conn.raddr.ip}:{conn.raddr.port}" if conn.raddr else ""
proc_name = ""
if conn.pid:
try:
proc_name = psutil.Process(conn.pid).name()
except (psutil.NoSuchProcess, psutil.AccessDenied):
proc_name = "?"
result.append({
"fd": conn.fd,
"family": str(conn.family),
"type": str(conn.type),
"local_addr": local,
"remote_addr": remote,
"status": conn.status,
"pid": conn.pid,
"process_name": proc_name,
})
return result
# ------------------------------------------------------------------
# Suspicious-connection detection
# ------------------------------------------------------------------
def find_suspicious_connections(self) -> List[Dict[str, Any]]:
"""Identify connections to known mining pools or suspicious ports.
Checks remote addresses against :pydata:`constants.CRYPTO_POOL_DOMAINS`
and :pydata:`constants.SUSPICIOUS_PORTS`.
"""
suspicious: List[Dict[str, Any]] = []
try:
connections = psutil.net_connections(kind="inet")
except psutil.AccessDenied:
logger.warning("Insufficient permissions to read network connections")
return suspicious
for conn in connections:
raddr = conn.raddr
if not raddr:
continue
remote_ip = raddr.ip
remote_port = raddr.port
local_str = f"{conn.laddr.ip}:{conn.laddr.port}" if conn.laddr else "?"
remote_str = f"{remote_ip}:{remote_port}"
proc_info = self.resolve_process_for_connection(conn)
# Suspicious port.
if remote_port in SUSPICIOUS_PORTS:
suspicious.append({
"local_addr": local_str,
"remote_addr": remote_str,
"pid": conn.pid,
"process": proc_info,
"status": conn.status,
"reason": f"Connection on known mining port {remote_port}",
"severity": "HIGH",
})
# Mining-pool domain (substring match on IP / hostname).
for domain in CRYPTO_POOL_DOMAINS:
if domain in remote_ip:
suspicious.append({
"local_addr": local_str,
"remote_addr": remote_str,
"pid": conn.pid,
"process": proc_info,
"status": conn.status,
"reason": f"Connection to known mining pool: {domain}",
"severity": "CRITICAL",
})
break
return suspicious
# ------------------------------------------------------------------
# Listening-port audit
# ------------------------------------------------------------------
@staticmethod
def check_listening_ports() -> List[Dict[str, Any]]:
"""Return listening sockets that are *not* in the expected-services list.
Unexpected listeners may indicate a backdoor or reverse shell.
"""
unexpected: List[Dict[str, Any]] = []
try:
connections = psutil.net_connections(kind="inet")
except psutil.AccessDenied:
logger.warning("Insufficient permissions to read network connections")
return unexpected
for conn in connections:
if conn.status != "LISTEN":
continue
port = conn.laddr.port if conn.laddr else None
if port is None:
continue
proc_name = ""
if conn.pid:
try:
proc_name = psutil.Process(conn.pid).name()
except (psutil.NoSuchProcess, psutil.AccessDenied):
proc_name = "?"
expected_name = _EXPECTED_LISTENERS.get(port)
if expected_name and expected_name in proc_name:
continue # known good
# Skip very common ephemeral / system ports when we can't resolve.
if port > 49152:
continue
if port not in _EXPECTED_LISTENERS:
unexpected.append({
"port": port,
"local_addr": f"{conn.laddr.ip}:{port}" if conn.laddr else f"?:{port}",
"pid": conn.pid,
"process_name": proc_name,
"reason": f"Unexpected listening service on port {port}",
"severity": "MEDIUM",
})
return unexpected
# ------------------------------------------------------------------
# Process resolution
# ------------------------------------------------------------------
@staticmethod
def resolve_process_for_connection(conn: Any) -> Dict[str, Any]:
"""Return basic process info for a ``psutil`` connection object.
Returns
-------
dict
``pid``, ``name``, ``cmdline``, ``username``.
"""
info: Dict[str, Any] = {
"pid": conn.pid,
"name": "",
"cmdline": [],
"username": "",
}
if not conn.pid:
return info
try:
proc = psutil.Process(conn.pid)
info["name"] = proc.name()
info["cmdline"] = proc.cmdline()
info["username"] = proc.username()
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
return info
# ------------------------------------------------------------------
# DNS audit
# ------------------------------------------------------------------
@staticmethod
def check_dns_queries() -> List[Dict[str, Any]]:
"""Audit ``/etc/resolv.conf`` for suspicious DNS server entries.
Malware sometimes rewrites ``resolv.conf`` to redirect DNS through an
attacker-controlled resolver, enabling man-in-the-middle attacks or
DNS-based C2 communication.
"""
issues: List[Dict[str, Any]] = []
resolv_path = Path("/etc/resolv.conf")
if not resolv_path.exists():
return issues
try:
content = resolv_path.read_text()
except PermissionError:
logger.warning("Cannot read /etc/resolv.conf")
return issues
nameserver_re = re.compile(r"^\s*nameserver\s+(\S+)", re.MULTILINE)
for match in nameserver_re.finditer(content):
server = match.group(1)
if server in _SUSPICIOUS_DNS_SERVERS:
issues.append({
"server": server,
"file": str(resolv_path),
"reason": f"Potentially suspicious DNS server: {server}",
"severity": "MEDIUM",
})
# Flag non-RFC1918 / non-loopback servers that look unusual.
if not (
server.startswith("127.")
or server.startswith("10.")
or server.startswith("192.168.")
or server.startswith("172.")
or server == "::1"
):
# External DNS — not inherently bad but worth logging if the
# admin didn't set it intentionally.
issues.append({
"server": server,
"file": str(resolv_path),
"reason": f"External DNS server configured: {server}",
"severity": "LOW",
})
return issues