serverscout 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,82 @@
1
+ Metadata-Version: 2.4
2
+ Name: serverscout
3
+ Version: 0.1.0
4
+ Summary: Lightweight SSH-based security scanner for Linux servers
5
+ License: MIT
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: paramiko
9
+ Requires-Dist: click
10
+ Requires-Dist: rich
11
+
12
+ # serverscout
13
+
14
+ Lightweight SSH-based security scanner for Linux servers.
15
+
16
+ Checks open ports, firewall rules, SSH config, and malware persistence — no agent required.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ pip install serverscout
22
+ ```
23
+
24
+ ## CLI usage
25
+
26
+ ```bash
27
+ serverscout --host 1.2.3.4 --key ~/.ssh/id_rsa
28
+ serverscout --host 1.2.3.4 --password mypass
29
+ ```
30
+
31
+ ## Python usage
32
+
33
+ ```python
34
+ from serverscout import scan
35
+
36
+ results = scan([
37
+ {"host": "1.2.3.4", "key": "~/.ssh/id_rsa"},
38
+ {"host": "5.6.7.8", "password": "mypass"},
39
+ ])
40
+ ```
41
+
42
+ Disable Rich output and process results yourself:
43
+
44
+ ```python
45
+ results = scan(servers, report=False)
46
+
47
+ for host, data in results.items():
48
+ if data["status"] == "error":
49
+ print(f"{host}: connection failed — {data['error']}")
50
+ continue
51
+ criticals = [
52
+ msg for section in data["results"].values()
53
+ for severity, msg in section
54
+ if severity == "critical"
55
+ ]
56
+ if criticals:
57
+ print(f"{host}: {len(criticals)} critical issues")
58
+ ```
59
+
60
+ ## Parallel scanning
61
+
62
+ ```python
63
+ from serverscout import scan
64
+ from concurrent.futures import ThreadPoolExecutor
65
+
66
+ servers = [{"host": f"10.0.0.{i}", "key": "~/.ssh/id_rsa"} for i in range(1, 20)]
67
+
68
+ with ThreadPoolExecutor(max_workers=10) as executor:
69
+ futures = [executor.submit(scan, [s], report=False) for s in servers]
70
+ results = {}
71
+ for f in futures:
72
+ results.update(f.result())
73
+ ```
74
+
75
+ ## What it checks
76
+
77
+ | Section | Checks |
78
+ |---|---|
79
+ | Ports | Dangerous ports exposed to the internet (Redis, Postgres, MongoDB, etc.) |
80
+ | Firewall | iptables/nftables rules, fail2ban |
81
+ | SSH Config | Root login, password auth, empty passwords, X11 forwarding |
82
+ | Persistence | Executables in /tmp, postgres crontab, known malware processes, suspicious outgoing connections |
@@ -0,0 +1,71 @@
1
+ # serverscout
2
+
3
+ Lightweight SSH-based security scanner for Linux servers.
4
+
5
+ Checks open ports, firewall rules, SSH config, and malware persistence — no agent required.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install serverscout
11
+ ```
12
+
13
+ ## CLI usage
14
+
15
+ ```bash
16
+ serverscout --host 1.2.3.4 --key ~/.ssh/id_rsa
17
+ serverscout --host 1.2.3.4 --password mypass
18
+ ```
19
+
20
+ ## Python usage
21
+
22
+ ```python
23
+ from serverscout import scan
24
+
25
+ results = scan([
26
+ {"host": "1.2.3.4", "key": "~/.ssh/id_rsa"},
27
+ {"host": "5.6.7.8", "password": "mypass"},
28
+ ])
29
+ ```
30
+
31
+ Disable Rich output and process results yourself:
32
+
33
+ ```python
34
+ results = scan(servers, report=False)
35
+
36
+ for host, data in results.items():
37
+ if data["status"] == "error":
38
+ print(f"{host}: connection failed — {data['error']}")
39
+ continue
40
+ criticals = [
41
+ msg for section in data["results"].values()
42
+ for severity, msg in section
43
+ if severity == "critical"
44
+ ]
45
+ if criticals:
46
+ print(f"{host}: {len(criticals)} critical issues")
47
+ ```
48
+
49
+ ## Parallel scanning
50
+
51
+ ```python
52
+ from serverscout import scan
53
+ from concurrent.futures import ThreadPoolExecutor
54
+
55
+ servers = [{"host": f"10.0.0.{i}", "key": "~/.ssh/id_rsa"} for i in range(1, 20)]
56
+
57
+ with ThreadPoolExecutor(max_workers=10) as executor:
58
+ futures = [executor.submit(scan, [s], report=False) for s in servers]
59
+ results = {}
60
+ for f in futures:
61
+ results.update(f.result())
62
+ ```
63
+
64
+ ## What it checks
65
+
66
+ | Section | Checks |
67
+ |---|---|
68
+ | Ports | Dangerous ports exposed to the internet (Redis, Postgres, MongoDB, etc.) |
69
+ | Firewall | iptables/nftables rules, fail2ban |
70
+ | SSH Config | Root login, password auth, empty passwords, X11 forwarding |
71
+ | Persistence | Executables in /tmp, postgres crontab, known malware processes, suspicious outgoing connections |
@@ -0,0 +1,22 @@
1
+ [build-system]
2
+ requires = ["setuptools>=70", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "serverscout"
7
+ version = "0.1.0"
8
+ description = "Lightweight SSH-based security scanner for Linux servers"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.11"
12
+ dependencies = [
13
+ "paramiko",
14
+ "click",
15
+ "rich",
16
+ ]
17
+
18
+ [project.scripts]
19
+ serverscout = "serverscout.cli:scan"
20
+
21
+ [tool.setuptools.packages.find]
22
+ where = ["."]
@@ -0,0 +1,80 @@
1
+ from .connector import Connector
2
+ from .checks.ports import Ports
3
+ from .checks.firewall import Firewall
4
+ from .checks.ssh_config import SSHConfig
5
+ from .checks.persistence import Persistence
6
+ from .reporter import Reporter
7
+ from rich.progress import Progress, SpinnerColumn, TextColumn
8
+
9
+
10
+ def scan(servers: list[dict], report: bool = True) -> dict:
11
+ """
12
+ Scan one or more servers for security vulnerabilities.
13
+
14
+ Args:
15
+ servers: list of dicts with keys:
16
+ host (required), user (default: root),
17
+ key, password, port (default: 22)
18
+ report: print Rich report to stdout (default: True)
19
+
20
+ Returns:
21
+ dict keyed by host with scan results per section
22
+
23
+ Example:
24
+ from serverscout import scan
25
+
26
+ results = scan([
27
+ {"host": "1.2.3.4", "key": "~/.ssh/id_rsa"},
28
+ {"host": "5.6.7.8", "password": "mypass"},
29
+ ])
30
+ """
31
+ all_results = {}
32
+
33
+ with Progress(
34
+ SpinnerColumn(),
35
+ TextColumn("[progress.description]{task.description}"),
36
+ transient=True,
37
+ ) as progress:
38
+ for server in servers:
39
+ host = server["host"]
40
+ task = progress.add_task(f"Connecting to {host}...", total=4)
41
+
42
+ conn_args = {
43
+ "host": host,
44
+ "user": server.get("user", "root"),
45
+ "key_path": server.get("key"),
46
+ "password": server.get("password"),
47
+ "port": server.get("port", 22),
48
+ }
49
+ try:
50
+ with Connector(**conn_args) as conn:
51
+ progress.update(task, description=f"[{host}] Checking ports...")
52
+ ports = Ports(conn).check_vulnerable_ports()
53
+ progress.advance(task)
54
+
55
+ progress.update(task, description=f"[{host}] Checking firewall...")
56
+ firewall = Firewall(conn).check()
57
+ progress.advance(task)
58
+
59
+ progress.update(task, description=f"[{host}] Checking SSH config...")
60
+ ssh = SSHConfig(conn).check()
61
+ progress.advance(task)
62
+
63
+ progress.update(task, description=f"[{host}] Checking persistence...")
64
+ persistence = Persistence(conn).check()
65
+ progress.advance(task)
66
+
67
+ results = {
68
+ "ports": ports,
69
+ "firewall": firewall,
70
+ "ssh_config": ssh,
71
+ "persistence": persistence,
72
+ }
73
+ if report:
74
+ Reporter(host, results).render()
75
+ all_results[host] = {"status": "ok", "results": results}
76
+
77
+ except Exception as e:
78
+ all_results[host] = {"status": "error", "error": str(e)}
79
+
80
+ return all_results
File without changes
@@ -0,0 +1,30 @@
1
+ class Firewall:
2
+ def __init__(self, conn):
3
+ self.conn = conn
4
+
5
+ def check(self):
6
+ findings = []
7
+
8
+ iptables = self.conn.run("iptables -L INPUT -n --line-numbers 2>/dev/null")
9
+ nft = self.conn.run("nft list ruleset 2>/dev/null")
10
+
11
+ if not iptables and not nft:
12
+ findings.append(("critical", "No firewall detected (neither iptables nor nftables)"))
13
+ return findings
14
+
15
+ if iptables:
16
+ if "ACCEPT" in iptables and "DROP" not in iptables and "REJECT" not in iptables:
17
+ findings.append(("warn", "iptables accepts all traffic, no DROP/REJECT rules found"))
18
+ else:
19
+ findings.append(("ok", "iptables is active with restrictive rules"))
20
+
21
+ if nft and not iptables:
22
+ findings.append(("ok", "nftables is active"))
23
+
24
+ fail2ban = self.conn.run("systemctl is-active fail2ban 2>/dev/null")
25
+ if fail2ban != "active":
26
+ findings.append(("warn", "fail2ban is not running"))
27
+ else:
28
+ findings.append(("ok", "fail2ban is active"))
29
+
30
+ return findings
@@ -0,0 +1,66 @@
1
+ class Persistence:
2
+ def __init__(self, conn):
3
+ self.conn = conn
4
+
5
+ def check(self):
6
+ findings = []
7
+
8
+ # check /tmp and /var/tmp for executable files
9
+ tmp_exec = self.conn.run("find /tmp /var/tmp -type f -executable 2>/dev/null")
10
+ if tmp_exec:
11
+ for f in tmp_exec.splitlines():
12
+ findings.append(("critical", f"Executable file in /tmp: {f}"))
13
+ else:
14
+ findings.append(("ok", "No executable files found in /tmp"))
15
+
16
+ # check crontab for all users with login shells
17
+ users = self.conn.run("getent passwd | awk -F: '$7 !~ /nologin|false/ {print $1}'")
18
+ for user in users.splitlines():
19
+ cron = self.conn.run(f"crontab -u {user} -l 2>/dev/null")
20
+ if cron and "no crontab" not in cron:
21
+ for line in cron.splitlines():
22
+ line = line.strip()
23
+ if not line or line.startswith("#"):
24
+ continue
25
+ findings.append(("warn", f"Crontab entry for {user}: {line}"))
26
+
27
+ # check for suspicious systemd user services under postgres
28
+ pg_systemd = self.conn.run("ls /var/lib/postgresql/.config/systemd/user/ 2>/dev/null")
29
+ if pg_systemd:
30
+ findings.append(("critical", f"Systemd user services found under postgres: {pg_systemd}"))
31
+ else:
32
+ findings.append(("ok", "No systemd user services found under postgres"))
33
+
34
+ # check .bashrc files for suspicious entries
35
+ for user_home in ["/root", "/var/lib/postgresql"]:
36
+ bashrc = self.conn.run(f"cat {user_home}/.bashrc 2>/dev/null")
37
+ suspicious = [
38
+ line for line in bashrc.splitlines()
39
+ if any(x in line for x in ["curl", "wget", "bash -i", "/tmp", "nc ", "ncat"])
40
+ ]
41
+ if suspicious:
42
+ findings.append(("critical", f"Suspicious entry in {user_home}/.bashrc: {suspicious[0]}"))
43
+
44
+ # check for known malware process names
45
+ malware_names = ["reflect_client", "reflect_restart", "kinsing", "kdevtmpfsi", "xmrig"]
46
+ procs = self.conn.run("ps aux")
47
+ for name in malware_names:
48
+ if name in procs:
49
+ findings.append(("critical", f"Suspicious process detected: {name}"))
50
+
51
+ # check for outgoing connections on known malware ports
52
+ suspicious_conns = self.conn.run(
53
+ "ss -tnp state established 2>/dev/null | awk 'NR>1 {print $4, $5}'"
54
+ )
55
+ for line in suspicious_conns.splitlines():
56
+ parts = line.split()
57
+ if len(parts) >= 2:
58
+ dst = parts[1]
59
+ try:
60
+ dst_port = int(dst.rsplit(":", 1)[-1])
61
+ except ValueError:
62
+ continue
63
+ if dst_port in (9999, 4444, 1337, 31337):
64
+ findings.append(("critical", f"Suspicious outgoing connection: {line}"))
65
+
66
+ return findings
@@ -0,0 +1,58 @@
1
+ SHOULD_NOT_BE_OPEN = {
2
+ 5432: "PostgreSQL",
3
+ 3306: "MySQL",
4
+ 6379: "Redis",
5
+ 9000: "ClickHouse native",
6
+ 9009: "ClickHouse HTTP",
7
+ 27017: "MongoDB",
8
+ 5984: "CouchDB",
9
+ 2379: "etcd",
10
+ 2380: "etcd peer",
11
+ 8500: "Consul",
12
+ 9200: "Elasticsearch",
13
+ 9300: "Elasticsearch cluster",
14
+ 5601: "Kibana",
15
+ 8888: "Dev server",
16
+ 9090: "Prometheus",
17
+ 3000: "Grafana",
18
+ 9100: "Node Exporter",
19
+ 23: "Telnet",
20
+ 111: "RPC",
21
+ 2049: "NFS",
22
+ 5672: "RabbitMQ",
23
+ 15672: "RabbitMQ management",
24
+ 9092: "Kafka",
25
+ }
26
+
27
+
28
+ class Ports:
29
+ def __init__(self, conn):
30
+ self.conn = conn
31
+
32
+ def get_open_ports(self):
33
+ output = self.conn.run("ss -tlnpu")
34
+ tcp, udp = [], []
35
+ for line in output.splitlines()[1:]:
36
+ parts = line.split()
37
+ if len(parts) < 5:
38
+ continue
39
+ proto = parts[0]
40
+ addr = parts[4]
41
+ port = int(addr.rsplit(":", 1)[-1])
42
+ if "*" in addr or "0.0.0.0" in addr or ":::" in addr:
43
+ (tcp if "tcp" in proto else udp).append(port)
44
+ return sorted(set(tcp)), sorted(set(udp))
45
+
46
+ def check_vulnerable_ports(self):
47
+ tcp, udp = self.get_open_ports()
48
+ vuln_tcp = {p: SHOULD_NOT_BE_OPEN[p] for p in tcp if p in SHOULD_NOT_BE_OPEN}
49
+ vuln_udp = {p: SHOULD_NOT_BE_OPEN[p] for p in udp if p in SHOULD_NOT_BE_OPEN}
50
+
51
+ findings = []
52
+ for port, name in vuln_tcp.items():
53
+ findings.append(("critical", f"TCP {port} ({name}) is exposed to the internet"))
54
+ for port, name in vuln_udp.items():
55
+ findings.append(("critical", f"UDP {port} ({name}) is exposed to the internet"))
56
+ if not findings:
57
+ findings.append(("ok", "No dangerous open ports detected"))
58
+ return findings
@@ -0,0 +1,34 @@
1
+ class SSHConfig:
2
+ def __init__(self, conn):
3
+ self.conn = conn
4
+
5
+ def _get(self, key: str) -> str:
6
+ return self.conn.run(f"sshd -T 2>/dev/null | grep -i '^{key} '").split()[-1].lower()
7
+
8
+ def check(self):
9
+ findings = []
10
+
11
+ checks = {
12
+ "permitrootlogin": ("prohibit-password", "warn", "Root login allowed via password"),
13
+ "passwordauthentication": ("no", "warn", "Password authentication is enabled"),
14
+ "permitemptypasswords": ("no", "critical", "Empty passwords are permitted"),
15
+ "x11forwarding": ("no", "warn", "X11 forwarding is enabled"),
16
+ }
17
+
18
+ for key, (safe_val, severity, msg) in checks.items():
19
+ try:
20
+ val = self._get(key)
21
+ if val != safe_val:
22
+ findings.append((severity, f"{msg} ({key} = {val})"))
23
+ else:
24
+ findings.append(("ok", f"{key} = {val}"))
25
+ except (IndexError, Exception):
26
+ findings.append(("warn", f"Could not retrieve {key}"))
27
+
28
+ port = self.conn.run("sshd -T 2>/dev/null | grep -i '^port '").split()[-1]
29
+ if port == "22":
30
+ findings.append(("warn", "SSH is running on default port 22"))
31
+ else:
32
+ findings.append(("ok", f"SSH is running on non-default port {port}"))
33
+
34
+ return findings
@@ -0,0 +1,62 @@
1
+ import json
2
+ import click
3
+ from .connector import Connector
4
+ from .checks.ports import Ports
5
+ from .checks.firewall import Firewall
6
+ from .checks.ssh_config import SSHConfig
7
+ from .checks.persistence import Persistence
8
+ from .reporter import Reporter
9
+ from rich.progress import Progress, SpinnerColumn, TextColumn
10
+ from rich.console import Console
11
+
12
+ console = Console()
13
+
14
+
15
+ @click.command()
16
+ @click.option("--host", required=True, help="Server IP or hostname")
17
+ @click.option("--user", default="root", help="SSH user (default: root)")
18
+ @click.option("--key", default=None, help="Path to private key")
19
+ @click.option("--password", default=None, help="SSH password")
20
+ @click.option("--port", default=22, show_default=True, help="SSH port")
21
+ @click.option("--json", "as_json", is_flag=True, help="Output results as JSON")
22
+ def scan(host, user, key, password, port, as_json):
23
+ """Scan a remote server for security vulnerabilities and suspicious activity."""
24
+ with Progress(
25
+ SpinnerColumn(),
26
+ TextColumn("[progress.description]{task.description}"),
27
+ transient=True,
28
+ ) as progress:
29
+ task = progress.add_task(f"Connecting to {host}...", total=4)
30
+
31
+ with Connector(host, user, key, password, port) as conn:
32
+ progress.update(task, description=f"[{host}] Checking ports...")
33
+ ports = Ports(conn).check_vulnerable_ports()
34
+ progress.advance(task)
35
+
36
+ progress.update(task, description=f"[{host}] Checking firewall...")
37
+ firewall = Firewall(conn).check()
38
+ progress.advance(task)
39
+
40
+ progress.update(task, description=f"[{host}] Checking SSH config...")
41
+ ssh = SSHConfig(conn).check()
42
+ progress.advance(task)
43
+
44
+ progress.update(task, description=f"[{host}] Checking persistence...")
45
+ persistence = Persistence(conn).check()
46
+ progress.advance(task)
47
+
48
+ results = {
49
+ "ports": ports,
50
+ "firewall": firewall,
51
+ "ssh_config": ssh,
52
+ "persistence": persistence,
53
+ }
54
+
55
+ if as_json:
56
+ click.echo(json.dumps({host: {"status": "ok", "results": results}}, indent=2))
57
+ else:
58
+ Reporter(host, results).render()
59
+
60
+
61
+ if __name__ == "__main__":
62
+ scan()
@@ -0,0 +1,37 @@
1
+ import paramiko
2
+
3
+
4
+ class Connector:
5
+ def __init__(self, host: str, user: str = "root", key_path: str = None, password: str = None, port: int = 22):
6
+ self.host = host
7
+ self.user = user
8
+ self.key_path = key_path
9
+ self.password = password
10
+ self.port = port
11
+ self.client = None
12
+
13
+ def connect(self):
14
+ self.client = paramiko.SSHClient()
15
+ self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
16
+ self.client.connect(
17
+ hostname=self.host,
18
+ port=self.port,
19
+ username=self.user,
20
+ key_filename=self.key_path,
21
+ password=self.password,
22
+ )
23
+ return self
24
+
25
+ def run(self, cmd: str) -> str:
26
+ _, stdout, _ = self.client.exec_command(cmd)
27
+ return stdout.read().decode().strip()
28
+
29
+ def disconnect(self):
30
+ if self.client:
31
+ self.client.close()
32
+
33
+ def __enter__(self):
34
+ return self.connect()
35
+
36
+ def __exit__(self, *_):
37
+ self.disconnect()
@@ -0,0 +1,60 @@
1
+ from rich.console import Console
2
+ from rich.table import Table
3
+ from rich import box
4
+ from rich.text import Text
5
+
6
+ console = Console()
7
+
8
+ ICONS = {"ok": "✓", "warn": "⚠", "critical": "✗"}
9
+ COLORS = {"ok": "green", "warn": "yellow", "critical": "red"}
10
+ SCORES = {"ok": 0, "warn": 1, "critical": 3}
11
+
12
+ SECTION_NAMES = {
13
+ "ports": "Ports",
14
+ "firewall": "Firewall",
15
+ "ssh_config": "SSH Config",
16
+ "persistence": "Malware / Persistence",
17
+ }
18
+
19
+
20
+ class Reporter:
21
+ def __init__(self, host: str, results: dict):
22
+ self.host = host
23
+ self.results = results
24
+
25
+ def render(self):
26
+ console.print(f"\n[bold]serverscout report — {self.host}[/bold]\n")
27
+
28
+ total_score = 0
29
+
30
+ for section, findings in self.results.items():
31
+ table = Table(
32
+ title=SECTION_NAMES.get(section, section),
33
+ box=box.SIMPLE_HEAVY,
34
+ show_header=False,
35
+ padding=(0, 1),
36
+ )
37
+ table.add_column("", width=2)
38
+ table.add_column("Description")
39
+
40
+ for severity, msg in findings:
41
+ color = COLORS[severity]
42
+ icon = ICONS[severity]
43
+ table.add_row(
44
+ Text(icon, style=f"bold {color}"),
45
+ Text(msg, style=color if severity != "ok" else "default"),
46
+ )
47
+ total_score += SCORES[severity]
48
+
49
+ console.print(table)
50
+
51
+ if total_score == 0:
52
+ verdict = "[bold green]All clean[/bold green]"
53
+ elif total_score <= 3:
54
+ verdict = "[bold yellow]Minor issues found[/bold yellow]"
55
+ elif total_score <= 8:
56
+ verdict = "[bold red]Needs attention[/bold red]"
57
+ else:
58
+ verdict = "[bold red on white]CRITICAL[/bold red on white]"
59
+
60
+ console.print(f"Total score: {total_score} — {verdict}\n")
@@ -0,0 +1,82 @@
1
+ Metadata-Version: 2.4
2
+ Name: serverscout
3
+ Version: 0.1.0
4
+ Summary: Lightweight SSH-based security scanner for Linux servers
5
+ License: MIT
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: paramiko
9
+ Requires-Dist: click
10
+ Requires-Dist: rich
11
+
12
+ # serverscout
13
+
14
+ Lightweight SSH-based security scanner for Linux servers.
15
+
16
+ Checks open ports, firewall rules, SSH config, and malware persistence — no agent required.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ pip install serverscout
22
+ ```
23
+
24
+ ## CLI usage
25
+
26
+ ```bash
27
+ serverscout --host 1.2.3.4 --key ~/.ssh/id_rsa
28
+ serverscout --host 1.2.3.4 --password mypass
29
+ ```
30
+
31
+ ## Python usage
32
+
33
+ ```python
34
+ from serverscout import scan
35
+
36
+ results = scan([
37
+ {"host": "1.2.3.4", "key": "~/.ssh/id_rsa"},
38
+ {"host": "5.6.7.8", "password": "mypass"},
39
+ ])
40
+ ```
41
+
42
+ Disable Rich output and process results yourself:
43
+
44
+ ```python
45
+ results = scan(servers, report=False)
46
+
47
+ for host, data in results.items():
48
+ if data["status"] == "error":
49
+ print(f"{host}: connection failed — {data['error']}")
50
+ continue
51
+ criticals = [
52
+ msg for section in data["results"].values()
53
+ for severity, msg in section
54
+ if severity == "critical"
55
+ ]
56
+ if criticals:
57
+ print(f"{host}: {len(criticals)} critical issues")
58
+ ```
59
+
60
+ ## Parallel scanning
61
+
62
+ ```python
63
+ from serverscout import scan
64
+ from concurrent.futures import ThreadPoolExecutor
65
+
66
+ servers = [{"host": f"10.0.0.{i}", "key": "~/.ssh/id_rsa"} for i in range(1, 20)]
67
+
68
+ with ThreadPoolExecutor(max_workers=10) as executor:
69
+ futures = [executor.submit(scan, [s], report=False) for s in servers]
70
+ results = {}
71
+ for f in futures:
72
+ results.update(f.result())
73
+ ```
74
+
75
+ ## What it checks
76
+
77
+ | Section | Checks |
78
+ |---|---|
79
+ | Ports | Dangerous ports exposed to the internet (Redis, Postgres, MongoDB, etc.) |
80
+ | Firewall | iptables/nftables rules, fail2ban |
81
+ | SSH Config | Root login, password auth, empty passwords, X11 forwarding |
82
+ | Persistence | Executables in /tmp, postgres crontab, known malware processes, suspicious outgoing connections |
@@ -0,0 +1,17 @@
1
+ README.md
2
+ pyproject.toml
3
+ serverscout/__init__.py
4
+ serverscout/cli.py
5
+ serverscout/connector.py
6
+ serverscout/reporter.py
7
+ serverscout.egg-info/PKG-INFO
8
+ serverscout.egg-info/SOURCES.txt
9
+ serverscout.egg-info/dependency_links.txt
10
+ serverscout.egg-info/entry_points.txt
11
+ serverscout.egg-info/requires.txt
12
+ serverscout.egg-info/top_level.txt
13
+ serverscout/checks/__init__.py
14
+ serverscout/checks/firewall.py
15
+ serverscout/checks/persistence.py
16
+ serverscout/checks/ports.py
17
+ serverscout/checks/ssh_config.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ serverscout = serverscout.cli:scan
@@ -0,0 +1,3 @@
1
+ paramiko
2
+ click
3
+ rich
@@ -0,0 +1,2 @@
1
+ dist
2
+ serverscout
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+