Add hardering

This commit is contained in:
Johnny
2026-04-06 08:37:54 +02:00
parent 4980d8cf3c
commit c0412d1150
27 changed files with 1527 additions and 82 deletions

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
import argparse
import os
import sys
from datetime import datetime
@@ -14,6 +15,20 @@ from .storage import ScenarioStore
from .system_info import detect_system
def ensure_root() -> None:
if os.geteuid() == 0:
return
if os.environ.get("SECURECHECK_SKIP_SUDO") == "1":
return
args = sys.argv[1:]
if getattr(sys, "frozen", False):
target = sys.argv[0]
cmd = ["sudo", "-E", target, *args]
else:
cmd = ["sudo", "-E", sys.executable, "-m", "securecheck", *args]
os.execvp("sudo", cmd)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="SecureCheck - console semi-graphique pour contrôles sécurité Linux")
parser.add_argument("--dry-run", action="store_true", help="Simule les commandes sans modifier le système")
@@ -66,6 +81,7 @@ def print_summary(results, run_log_path, system) -> None:
def main() -> int:
ensure_root()
args = parse_args()
paths = build_paths()
ensure_app_dirs(paths)
@@ -77,6 +93,7 @@ def main() -> int:
store = ScenarioStore(paths.scenario_file, builtin_scenarios())
if args.list_scenarios:
print(f"Scénarios stockés dans {paths.scenario_file}")
for scenario in store.list_all():
print(f"{scenario.name}: {scenario.description}")
return 0

Binary file not shown.

View File

@@ -1,7 +1,6 @@
from __future__ import annotations
import curses
import re
import textwrap
from collections import defaultdict
from dataclasses import dataclass
@@ -11,6 +10,7 @@ from .assets import banner_text
from .models import Scenario, TaskDefinition, TaskResult
from .status import StatusItem
from .storage import ScenarioStore
from .summary_utils import SCORE_PREFIXES, clean_text, collect_details
from .system_info import SystemInfo
@@ -334,18 +334,12 @@ class SecureCheckTUI:
class RunSummaryTUI:
ANSI_RE = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]")
def __init__(self, results: list[TaskResult], status_items: list[StatusItem], run_log_path: str) -> None:
self.results = results
self.status_items = status_items
self.run_log_path = run_log_path
self.scroll_offset = 0
@classmethod
def _clean(cls, text: str) -> str:
return cls.ANSI_RE.sub("", text)
def run(self) -> None:
curses.wrapper(self._main)
@@ -368,8 +362,7 @@ class RunSummaryTUI:
height, width = stdscr.getmaxyx()
ok_count = sum(1 for result in self.results if result.success)
ko_count = len(self.results) - ok_count
score_lines: list[str] = []
notif_lines: list[str] = []
score_lines, notif_lines = collect_details(self.results)
entries: list[tuple[str, int]] = []
entries.append((f"OK: {ok_count} | ECHEC: {ko_count} | Appuie sur une touche pour revenir", curses.color_pair(Palette.HEADER) | curses.A_BOLD))
for result in self.results:
@@ -377,12 +370,10 @@ class RunSummaryTUI:
color = curses.color_pair(Palette.SUCCESS if result.success else Palette.ERROR)
entries.append((f"{status:<4} {result.label} ({result.duration_seconds:.1f}s)", color | curses.A_BOLD))
for detail in result.details:
clean = self._clean(detail)
if clean.startswith("Score Lynis") or clean.startswith("Hardening index"):
score_lines.append(clean)
clean = clean_text(detail).strip()
if any(clean.startswith(prefix) for prefix in SCORE_PREFIXES):
continue
if clean.startswith("Modifications") or clean.strip().startswith(""):
notif_lines.append(clean)
if clean.startswith("Modifications") or clean.startswith(""):
continue
wrapped = textwrap.wrap(clean, width - 9) or [""]
for line in wrapped:

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,7 @@ from .tasks import (
log_rotation,
lynis_audit,
rootkit_check,
system_hardening,
system_update,
utilities_setup,
zram_setup,
@@ -98,6 +99,14 @@ def task_catalog() -> list[TaskDefinition]:
handler=lambda context: None,
default_selected=False,
),
TaskDefinition(
key="system_hardening",
label="Durcissement système (hardening)",
description="Applique un durcissement complet : sysctl, SSH, PAM, modules noyau, permissions, AIDE, bannières, limites de sécurité.",
category="Sécurité",
handler=lambda context: None,
default_selected=False,
),
]
handlers = {
@@ -111,6 +120,7 @@ def task_catalog() -> list[TaskDefinition]:
"zram_setup": zram_setup,
"firewall_setup": firewall_setup,
"docker_setup": docker_setup,
"system_hardening": system_hardening,
}
return [bind(task, handlers[task.key]) for task in base]

View File

@@ -1,15 +1,28 @@
from __future__ import annotations
import logging
import os
from logging.handlers import RotatingFileHandler
from pathlib import Path
_LOG_FILE_MODE = 0o644
def _prepare_log_file(path: Path) -> None:
"""Crée le fichier de log s'il n'existe pas et fixe ses permissions à 644."""
path.parent.mkdir(parents=True, exist_ok=True)
if not path.exists():
path.touch()
os.chmod(path, _LOG_FILE_MODE)
def setup_logging(log_file: Path) -> logging.Logger:
logger = logging.getLogger("securecheck")
if logger.handlers:
return logger
_prepare_log_file(log_file)
logger.setLevel(logging.INFO)
logger.propagate = False
formatter = logging.Formatter(
@@ -24,6 +37,8 @@ def setup_logging(log_file: Path) -> logging.Logger:
def attach_run_handler(logger: logging.Logger, run_log_file: Path) -> RotatingFileHandler:
_prepare_log_file(run_log_file)
formatter = logging.Formatter(
fmt="%(asctime)s | %(levelname)s | %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",

View File

@@ -91,12 +91,10 @@ def collect_status(system: SystemInfo) -> list[StatusItem]:
docker_active = _command_exists("docker") and _service_active("docker.service")
fail2ban_active = _command_exists("fail2ban-client") and _service_active("fail2ban.service")
services.append(StatusItem("Services", "Service Docker", docker_active, "actif" if docker_active else "inactif"))
services.append(StatusItem("Services", "Service fail2ban", fail2ban_active, "actif" if fail2ban_active else "inactif"))
services.append(StatusItem("Services", "Docker", docker_active, "actif" if docker_active else "inactif"))
services.append(StatusItem("Services", "fail2ban", fail2ban_active, "actif" if fail2ban_active else "inactif"))
avahi_running = _command_exists("avahi-daemon") and _service_active("avahi-daemon")
services.append(StatusItem("Services", "Avahi", not avahi_running, "désactivé" if not avahi_running else "actif"))
services.append(_binary_status("Services", "Docker", "docker"))
services.append(_binary_status("Services", "fail2ban", "fail2ban-client"))
poste.extend([
_binary_status("Poste", "zsh", "zsh"),

View File

@@ -0,0 +1,41 @@
from __future__ import annotations
import re
from typing import Sequence
from .models import TaskResult
ANSI_RE = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]")
SCORE_PREFIXES = ("Score Lynis", "Hardening index")
RECOMMENDATION_PREFIXES = (
"Modifications recommandées",
"",
)
def clean_text(text: str) -> str:
return ANSI_RE.sub("", text)
def collect_details(results: Sequence[TaskResult]) -> tuple[list[str], list[str]]:
score_lines: list[str] = []
recommendation_lines: list[str] = []
seen_scores: set[str] = set()
seen_recommendations: set[str] = set()
for result in results:
for detail in result.details:
line = clean_text(detail).strip()
if not line:
continue
if any(line.startswith(prefix) for prefix in SCORE_PREFIXES) and line not in seen_scores:
seen_scores.add(line)
score_lines.append(line)
continue
if any(line.startswith(prefix) for prefix in RECOMMENDATION_PREFIXES) and line not in seen_recommendations:
seen_recommendations.add(line)
recommendation_lines.append(line)
return score_lines, recommendation_lines

View File

@@ -1,7 +1,10 @@
from __future__ import annotations
import json
import os
import re
import stat
import tempfile
from datetime import datetime
from pathlib import Path
@@ -195,8 +198,21 @@ def rootkit_check(context: ExecutionContext, task: TaskDefinition) -> TaskResult
report_path = context.paths.report_dir / f"{datetime.now().strftime('%Y%m%d-%H%M%S')}-rootkit-report.json"
context.runner.write_text_file(report_path, json.dumps(report_payload, indent=2) + "\n", mode=0o640, requires_root=False)
details.append(f"Rapport rootkits: {report_path}")
success = rkhunter_result.returncode == 0 and chkrootkit_result.returncode == 0
return context.make_result(task, success=success, changed=changed, started_at=started_at, details=details, error=None if success else "Vérification rootkit incomplète ou avec alertes")
# rkhunter: rc=0 clean, rc=1 warnings trouvés, rc=2+ erreur critique (outil n'a pas pu tourner)
rkhunter_ran = rkhunter_result.returncode <= 1
rkhunter_warnings = [line for line in rkhunter_result.stdout.splitlines() if line.startswith("Warning:")]
if rkhunter_warnings:
details.append(f"rkhunter: {len(rkhunter_warnings)} warning(s) à vérifier dans le rapport")
# chkrootkit: rc=0 signifie que l'outil a tourné (pas qu'il n'y a pas d'infection — les INFECTED sont dans stdout)
chkrootkit_ran = chkrootkit_result.returncode == 0
infected_lines = [line for line in chkrootkit_result.stdout.splitlines() if "INFECTED" in line]
if infected_lines:
details.append(f"chkrootkit: {len(infected_lines)} détection(s) à vérifier dans le rapport")
success = rkhunter_ran and chkrootkit_ran
error = None if success else "Les outils de vérification n'ont pas pu s'exécuter correctement"
return context.make_result(task, success=success, changed=changed, started_at=started_at, details=details, error=error)
def log_rotation(context: ExecutionContext, task: TaskDefinition) -> TaskResult:
@@ -434,7 +450,6 @@ def utilities_setup(context: ExecutionContext, task: TaskDefinition) -> TaskResu
context.runner.run(["cp", "-f", str(aide_db_new), "/var/lib/aide/aide.db"], requires_root=True, check=False)
details.append("Base AIDE mise à jour")
if context.runner.command_exists("systemctl"):
context.runner.run(["systemctl", "enable", "--now", "aidecheck.timer"], requires_root=True, check=False)
context.runner.run(["systemctl", "enable", "--now", "dailyaidecheck.timer"], requires_root=True, check=False)
details.append("Timers AIDE activés")
@@ -560,6 +575,83 @@ def docker_setup(context: ExecutionContext, task: TaskDefinition) -> TaskResult:
return _result(context, task, started_at, changed=changed, details=details)
def system_hardening(context: ExecutionContext, task: TaskDefinition) -> TaskResult:
started_at = datetime.now()
details: list[str] = []
script_content = asset_text("system_hardening.sh")
tmp_dir = context.paths.state_dir
tmp_dir.mkdir(parents=True, exist_ok=True)
script_path: Path | None = None
try:
with tempfile.NamedTemporaryFile(
mode="w", suffix=".sh", delete=False, dir=tmp_dir, encoding="utf-8"
) as fh:
fh.write(script_content)
script_path = Path(fh.name)
current_mode = stat.S_IMODE(script_path.stat().st_mode)
script_path.chmod(current_mode | 0o111)
env = os.environ.copy()
env.update({
"AUTO_YES": "yes",
"AUTO_SSH_PORT": "22",
"AUTO_CHANGE_ROOT_PWD": "no",
"AUTO_DISABLE_ROOT_LOGIN": "no",
"AUTO_SKIP_LYNIS": "no",
"AUTO_ENABLE_FAIL2BAN": "yes",
"AUTO_ENABLE_UFW": "yes",
"AUTO_ENABLE_AIDE": "yes",
"AUTO_ENABLE_CLAMAV": "yes",
"AUTO_SKIP_PORTS_DETECTION": "no",
"DEBIAN_FRONTEND": "noninteractive",
})
result = context.runner.run(
["bash", str(script_path), "--unattended"],
requires_root=True,
check=False,
capture_output=True,
env=env,
)
ok_steps = [line for line in result.stdout.splitlines() if "[OK]" in line]
warn_steps = [line for line in result.stdout.splitlines() if "[WARN]" in line]
err_steps = [line for line in result.stdout.splitlines() if "[ERR]" in line]
details.append(f"{len(ok_steps)} étape(s) réussie(s)")
if warn_steps:
details.append(f"{len(warn_steps)} avertissement(s)")
if err_steps:
details.append(f"{len(err_steps)} erreur(s)")
for line in err_steps[:5]:
details.append(f" {line.strip()}")
score_line = next(
(line for line in result.stdout.splitlines() if "Score:" in line and "Lynis" in line),
None,
)
if score_line:
details.append(score_line.strip())
log_path = Path("/var/log/system_hardening.log")
if log_path.exists():
details.append(f"Log complet: {log_path}")
backup_path = next(Path("/root").glob("backup_hardening_*"), None) if Path("/root").exists() else None
if backup_path:
details.append(f"Sauvegardes: {backup_path}")
success = result.returncode == 0 and not err_steps
error = None if success else f"Le script s'est terminé avec le code {result.returncode}"
return context.make_result(task, success=success, changed=True, started_at=started_at, details=details, error=error)
finally:
if script_path and script_path.exists():
script_path.unlink(missing_ok=True)
def bind(task: TaskDefinition, func) -> TaskDefinition:
return TaskDefinition(
key=task.key,