first commit

This commit is contained in:
2026-07-11 23:54:36 +07:00
commit b07562921b
40 changed files with 3813 additions and 0 deletions

229
modules/ssh/ssh_security.py Normal file
View File

@@ -0,0 +1,229 @@
import os
import shlex
import stat
from pathlib import Path
from core.base import BaseWorker
from core.schemas.results import AuditFindings, AuditResults
from core.schemas.status import AuditSeverity, AuditStatus
from .config import SSHModuleConfig
class SSHSecurityWorker(BaseWorker):
config_model = SSHModuleConfig
def __init__(self, config: SSHModuleConfig | None = None) -> None:
super().__init__(config)
self.module_config = config or SSHModuleConfig()
def run(self) -> AuditResults:
if not self.module_config.enabled:
return AuditResults(
status=AuditStatus.SKIPPED,
findings=[],
risk_level=0.0,
)
if self.module_config.test_failure:
findings = [
AuditFindings(
name="im fucking dumbass rragh",
description="im testing!.",
severity=AuditSeverity.CRITICAL,
)
]
return AuditResults(
status=AuditStatus.PASS if not findings else AuditStatus.FAIL,
findings=findings,
risk_level=self._calculate_risk(findings),
)
if os.name != "posix":
return AuditResults(
status=AuditStatus.SKIPPED,
findings=[],
risk_level=0.0,
)
sshd_config = Path("/etc/ssh/sshd_config")
if not sshd_config.exists():
return AuditResults(
status=AuditStatus.SKIPPED,
findings=[
AuditFindings(
name="sshd_config_not_found",
description="SSH server config /etc/ssh/sshd_config was not found.",
severity=AuditSeverity.LOW,
)
],
risk_level=0.1,
)
findings: list[AuditFindings] = []
resolved_config = self._read_merged_config(sshd_config)
findings.extend(self._check_config_directives(resolved_config))
findings.extend(self._check_config_permissions(sshd_config))
findings.extend(self._check_host_key_permissions(Path("/etc/ssh")))
return AuditResults(
status=AuditStatus.PASS if not findings else AuditStatus.FAIL,
findings=findings,
risk_level=self._calculate_risk(findings),
)
def _read_merged_config(self, root_config: Path) -> dict[str, str]:
config_files = [root_config]
include_dir = root_config.parent / "sshd_config.d"
if include_dir.exists() and include_dir.is_dir():
config_files.extend(sorted(include_dir.glob("*.conf")))
directives: dict[str, str] = {}
for config_file in config_files:
for raw_line in config_file.read_text(encoding="utf-8").splitlines():
line = raw_line.strip()
if not line or line.startswith("#"):
continue
line = line.split("#", 1)[0].strip()
if not line:
continue
parts = shlex.split(line, comments=False, posix=True)
if len(parts) < 2: # noqa: PLR2004
continue
directive = parts[0].lower()
value = " ".join(parts[1:])
directives[directive] = value
return directives
def _check_config_directives(self, config: dict[str, str]) -> list[AuditFindings]:
findings: list[AuditFindings] = []
if config.get("permitrootlogin", "prohibit-password").lower() not in {
"no",
"forced-commands-only",
}:
findings.append(
AuditFindings(
name="permit_root_login_enabled",
description="Directive PermitRootLogin should be set to 'no' or 'forced-commands-only'.",
severity=AuditSeverity.CRITICAL,
)
)
if config.get("passwordauthentication", "yes").lower() != "no":
findings.append(
AuditFindings(
name="password_authentication_enabled",
description="Directive PasswordAuthentication should be disabled to prefer key-based access.",
severity=AuditSeverity.HIGH,
)
)
if config.get("pubkeyauthentication", "yes").lower() != "yes":
findings.append(
AuditFindings(
name="pubkey_authentication_disabled",
description="Directive PubkeyAuthentication should be enabled.",
severity=AuditSeverity.HIGH,
)
)
if config.get("x11forwarding", "no").lower() != "no":
findings.append(
AuditFindings(
name="x11_forwarding_enabled",
description="Directive X11Forwarding should usually be disabled on hardened servers.",
severity=AuditSeverity.MEDIUM,
)
)
max_auth_tries = config.get("maxauthtries", "6")
if max_auth_tries.isdigit() and int(max_auth_tries) > 4: # noqa: PLR2004
findings.append(
AuditFindings(
name="max_auth_tries_too_high",
description="Directive MaxAuthTries should be 4 or less.",
severity=AuditSeverity.MEDIUM,
)
)
if config.get("permitemptypasswords", "no").lower() != "no":
findings.append(
AuditFindings(
name="empty_passwords_permitted",
description="Directive PermitEmptyPasswords must be disabled.",
severity=AuditSeverity.CRITICAL,
)
)
return findings
def _check_config_permissions(self, config_path: Path) -> list[AuditFindings]:
findings: list[AuditFindings] = []
file_stat = config_path.stat()
mode = stat.S_IMODE(file_stat.st_mode)
if file_stat.st_uid != 0:
findings.append(
AuditFindings(
name="sshd_config_not_owned_by_root",
description="File /etc/ssh/sshd_config should be owned by root.",
severity=AuditSeverity.HIGH,
)
)
if mode & stat.S_IWGRP or mode & stat.S_IWOTH:
findings.append(
AuditFindings(
name="sshd_config_writable_by_non_root",
description="File /etc/ssh/sshd_config must not be writable by group or others.",
severity=AuditSeverity.HIGH,
)
)
return findings
def _check_host_key_permissions(self, ssh_dir: Path) -> list[AuditFindings]:
findings: list[AuditFindings] = []
for key_path in sorted(ssh_dir.glob("ssh_host_*_key")):
file_stat = key_path.stat()
mode = stat.S_IMODE(file_stat.st_mode)
if file_stat.st_uid != 0:
findings.append(
AuditFindings(
name=f"{key_path.name}_not_owned_by_root",
description=f"Private host key {key_path} should be owned by root.",
severity=AuditSeverity.HIGH,
)
)
if mode & (stat.S_IRWXG | stat.S_IRWXO):
findings.append(
AuditFindings(
name=f"{key_path.name}_permissions_too_open",
description=f"Private host key {key_path} should not be accessible to group or others.",
severity=AuditSeverity.CRITICAL,
)
)
return findings
def _calculate_risk(self, findings: list[AuditFindings]) -> float:
if not findings:
return 0.0
severity_scores = {
AuditSeverity.CRITICAL: 0.45,
AuditSeverity.HIGH: 0.3,
AuditSeverity.MEDIUM: 0.2,
AuditSeverity.LOW: 0.1,
}
risk = sum(severity_scores[finding.severity] for finding in findings)
return min(1.0, risk)