diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e507e22 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,75 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Commandes de développement + +```bash +# Lancer l'application (mode interactif, demande sudo automatiquement) +python3 -m securecheck + +# Lancer sans sudo (pour tester sans élévation) +SECURECHECK_SKIP_SUDO=1 python3 -m securecheck + +# Mode non interactif +python3 -m securecheck --dry-run --tasks system_update,lynis_audit +python3 -m securecheck --scenario baseline_workstation --run +python3 -m securecheck --list-scenarios + +# Avec sudo (recommandé pour les tâches système réelles) +sudo -E python3 -m securecheck + +# Installation locale +pip install . + +# Build du binaire autonome (nécessite .venv avec pyinstaller) +python3 -m venv .venv +.venv/bin/pip install pyinstaller +./build_executable.sh +# Résultat dans dist/securecheck +``` + +## Architecture + +### Flux d'exécution principal + +`__main__.py:main()` orchestre tout : +1. `ensure_root()` — ré-exécute automatiquement avec `sudo -E` si non root (sauf `SECURECHECK_SKIP_SUDO=1`) +2. `detect_system()` — détecte distro, gestionnaire de paquets, user invocant +3. `task_catalog()` + `builtin_scenarios()` — charge tâches et scénarios +4. En mode interactif : `SecureCheckTUI` → sélection → `execute_tasks()` → `RunSummaryTUI` +5. En mode non interactif : résolution des tâches via `--tasks`/`--scenario` → `execute_tasks()` → affichage terminal + +### Modules clés + +- **`models.py`** — dataclasses centrales : `TaskDefinition`, `TaskResult`, `Scenario` +- **`catalog.py`** — registre de toutes les tâches (`task_catalog()`) et scénarios builtin (`builtin_scenarios()`). Utilise `bind()` pour attacher les handlers. +- **`tasks.py`** — implémentation de chaque tâche (une fonction par tâche). Chaque fonction reçoit `(context: ExecutionContext, task: TaskDefinition)` et retourne `TaskResult`. +- **`executor.py`** — `ExecutionContext` (contexte partagé par toutes les tâches) et `CommandRunner` (abstraction pour toutes les opérations système : paquets, fichiers, services, shell). `execute_tasks()` itère sur les tâches et capture les exceptions. +- **`app.py`** — TUI curses : `SecureCheckTUI` (menu principal) et `RunSummaryTUI` (résumé post-exécution) +- **`config.py`** — `AppPaths` et `build_paths()` : résolution XDG des chemins (config, state, logs). Logs dans `/var/log/securecheck` si root, sinon `~/.local/state/securecheck/logs`. +- **`storage.py`** — `ScenarioStore` : lecture/écriture des scénarios utilisateur dans `~/.config/securecheck/scenarios.json` +- **`system_info.py`** — `SystemInfo` + `detect_system()` : détecte OS, package manager, user réel (via `SUDO_USER`), home, uid/gid +- **`status.py`** — `collect_status()` : sonde les services/composants pour le tableau d'état LED +- **`assets.py`** — accès aux fichiers embarqués dans `securecheck/assets/` (banner.txt, p10k.zsh, icônes) + +### Ajouter une nouvelle tâche + +1. Écrire la fonction dans `tasks.py` avec la signature `(context: ExecutionContext, task: TaskDefinition) -> TaskResult` +2. Enregistrer dans `catalog.py` : ajouter un `TaskDefinition` dans `task_catalog()` et mapper son handler dans `handlers` +3. L'ajouter optionnellement à un `builtin_scenarios()` existant + +### Conventions importantes + +- `CommandRunner` est le seul point d'entrée pour les opérations système (jamais `subprocess` directement dans `tasks.py`) +- `context.runner.write_text_file()` est idempotent : ne réécrit pas si le contenu est identique +- `context.runner.update_package_index()` est appelé une seule fois par exécution (flag `_package_index_updated`) +- Le `target_user` dans `SystemInfo` est l'utilisateur réel ayant lancé `sudo`, pas root +- `--dry-run` : toutes les opérations système sont simulées, les commandes sont loggées sans être exécutées + +### Emplacements runtime + +- Scénarios : `~/.config/securecheck/scenarios.json` +- Logs : `/var/log/securecheck/` (root) ou `~/.local/state/securecheck/logs/` +- Rapports par exécution : `/run-YYYYMMDD-HHMMSS.log` +- Rapports tâches (lynis, rootkits) : `/reports/` diff --git a/__pycache__/run.cpython-313.pyc b/__pycache__/run.cpython-313.pyc new file mode 100644 index 0000000..0fa16f0 Binary files /dev/null and b/__pycache__/run.cpython-313.pyc differ diff --git a/build/securecheck/Analysis-00.toc b/build/securecheck/Analysis-00.toc index aa0d75b..c1977c3 100644 --- a/build/securecheck/Analysis-00.toc +++ b/build/securecheck/Analysis-00.toc @@ -31,6 +31,9 @@ 'DATA'), ('securecheck/assets/securecheck-icon.svg', '/home/tuxgyver/scripts/securecheck/securecheck/assets/securecheck-icon.svg', + 'DATA'), + ('securecheck/assets/system_hardening.sh', + '/home/tuxgyver/scripts/securecheck/securecheck/assets/system_hardening.sh', 'DATA')], '3.13.7 (main, Mar 3 2026, 12:19:54) [GCC 15.2.0]', [('pyi_rth_inspect', @@ -189,12 +192,12 @@ ('opcode', '/usr/lib/python3.13/opcode.py', 'PYMODULE'), ('_opcode_metadata', '/usr/lib/python3.13/_opcode_metadata.py', 'PYMODULE'), ('ast', '/usr/lib/python3.13/ast.py', 'PYMODULE'), + ('_py_abc', '/usr/lib/python3.13/_py_abc.py', 'PYMODULE'), + ('stringprep', '/usr/lib/python3.13/stringprep.py', 'PYMODULE'), + ('_colorize', '/usr/lib/python3.13/_colorize.py', 'PYMODULE'), + ('tracemalloc', '/usr/lib/python3.13/tracemalloc.py', 'PYMODULE'), ('subprocess', '/usr/lib/python3.13/subprocess.py', 'PYMODULE'), ('signal', '/usr/lib/python3.13/signal.py', 'PYMODULE'), - ('stringprep', '/usr/lib/python3.13/stringprep.py', 'PYMODULE'), - ('tracemalloc', '/usr/lib/python3.13/tracemalloc.py', 'PYMODULE'), - ('_colorize', '/usr/lib/python3.13/_colorize.py', 'PYMODULE'), - ('_py_abc', '/usr/lib/python3.13/_py_abc.py', 'PYMODULE'), ('securecheck.__main__', '/home/tuxgyver/scripts/securecheck/securecheck/__main__.py', 'PYMODULE'), @@ -250,6 +253,9 @@ ('securecheck.app', '/home/tuxgyver/scripts/securecheck/securecheck/app.py', 'PYMODULE'), + ('securecheck.summary_utils', + '/home/tuxgyver/scripts/securecheck/securecheck/summary_utils.py', + 'PYMODULE'), ('curses', '/usr/lib/python3.13/curses/__init__.py', 'PYMODULE'), ('curses.has_key', '/usr/lib/python3.13/curses/has_key.py', 'PYMODULE'), ('__future__', '/usr/lib/python3.13/__future__.py', 'PYMODULE')], @@ -307,8 +313,8 @@ ('python3.13/lib-dynload/_curses.cpython-313-x86_64-linux-gnu.so', '/usr/lib/python3.13/lib-dynload/_curses.cpython-313-x86_64-linux-gnu.so', 'EXTENSION'), - ('libz.so.1', '/lib/x86_64-linux-gnu/libz.so.1', 'BINARY'), ('libexpat.so.1', '/lib/x86_64-linux-gnu/libexpat.so.1', 'BINARY'), + ('libz.so.1', '/lib/x86_64-linux-gnu/libz.so.1', 'BINARY'), ('libcrypto.so.3', '/lib/x86_64-linux-gnu/libcrypto.so.3', 'BINARY'), ('libzstd.so.1', '/lib/x86_64-linux-gnu/libzstd.so.1', 'BINARY'), ('liblzma.so.5', '/lib/x86_64-linux-gnu/liblzma.so.5', 'BINARY'), @@ -333,33 +339,49 @@ ('securecheck/assets/securecheck-icon.svg', '/home/tuxgyver/scripts/securecheck/securecheck/assets/securecheck-icon.svg', 'DATA'), - ('wheel-0.46.1.dist-info/METADATA', - '/usr/lib/python3/dist-packages/wheel-0.46.1.dist-info/METADATA', - 'DATA'), - ('wheel-0.46.1.dist-info/WHEEL', - '/usr/lib/python3/dist-packages/wheel-0.46.1.dist-info/WHEEL', - 'DATA'), - ('wheel-0.46.1.dist-info/INSTALLER', - '/usr/lib/python3/dist-packages/wheel-0.46.1.dist-info/INSTALLER', + ('securecheck/assets/system_hardening.sh', + '/home/tuxgyver/scripts/securecheck/securecheck/assets/system_hardening.sh', 'DATA'), ('wheel-0.46.1.dist-info/entry_points.txt', '/usr/lib/python3/dist-packages/wheel-0.46.1.dist-info/entry_points.txt', 'DATA'), + ('wheel-0.46.1.dist-info/INSTALLER', + '/usr/lib/python3/dist-packages/wheel-0.46.1.dist-info/INSTALLER', + 'DATA'), + ('wheel-0.46.1.dist-info/WHEEL', + '/usr/lib/python3/dist-packages/wheel-0.46.1.dist-info/WHEEL', + 'DATA'), + ('wheel-0.46.1.dist-info/METADATA', + '/usr/lib/python3/dist-packages/wheel-0.46.1.dist-info/METADATA', + 'DATA'), ('base_library.zip', '/home/tuxgyver/scripts/securecheck/build/securecheck/base_library.zip', 'DATA')], - [('functools', '/usr/lib/python3.13/functools.py', 'PYMODULE'), - ('sre_compile', '/usr/lib/python3.13/sre_compile.py', 'PYMODULE'), - ('copyreg', '/usr/lib/python3.13/copyreg.py', 'PYMODULE'), - ('operator', '/usr/lib/python3.13/operator.py', 'PYMODULE'), - ('genericpath', '/usr/lib/python3.13/genericpath.py', 'PYMODULE'), - ('keyword', '/usr/lib/python3.13/keyword.py', 'PYMODULE'), + [('locale', '/usr/lib/python3.13/locale.py', 'PYMODULE'), + ('abc', '/usr/lib/python3.13/abc.py', 'PYMODULE'), + ('ntpath', '/usr/lib/python3.13/ntpath.py', 'PYMODULE'), ('types', '/usr/lib/python3.13/types.py', 'PYMODULE'), - ('sre_parse', '/usr/lib/python3.13/sre_parse.py', 'PYMODULE'), - ('os', '/usr/lib/python3.13/os.py', 'PYMODULE'), - ('_collections_abc', '/usr/lib/python3.13/_collections_abc.py', 'PYMODULE'), + ('linecache', '/usr/lib/python3.13/linecache.py', 'PYMODULE'), + ('posixpath', '/usr/lib/python3.13/posixpath.py', 'PYMODULE'), + ('collections', '/usr/lib/python3.13/collections/__init__.py', 'PYMODULE'), ('io', '/usr/lib/python3.13/io.py', 'PYMODULE'), - ('locale', '/usr/lib/python3.13/locale.py', 'PYMODULE'), + ('re._parser', '/usr/lib/python3.13/re/_parser.py', 'PYMODULE'), + ('re._constants', '/usr/lib/python3.13/re/_constants.py', 'PYMODULE'), + ('re._compiler', '/usr/lib/python3.13/re/_compiler.py', 'PYMODULE'), + ('re._casefix', '/usr/lib/python3.13/re/_casefix.py', 'PYMODULE'), + ('re', '/usr/lib/python3.13/re/__init__.py', 'PYMODULE'), + ('operator', '/usr/lib/python3.13/operator.py', 'PYMODULE'), + ('_collections_abc', '/usr/lib/python3.13/_collections_abc.py', 'PYMODULE'), + ('sre_constants', '/usr/lib/python3.13/sre_constants.py', 'PYMODULE'), + ('reprlib', '/usr/lib/python3.13/reprlib.py', 'PYMODULE'), + ('keyword', '/usr/lib/python3.13/keyword.py', 'PYMODULE'), + ('codecs', '/usr/lib/python3.13/codecs.py', 'PYMODULE'), + ('genericpath', '/usr/lib/python3.13/genericpath.py', 'PYMODULE'), + ('enum', '/usr/lib/python3.13/enum.py', 'PYMODULE'), + ('sre_parse', '/usr/lib/python3.13/sre_parse.py', 'PYMODULE'), + ('_weakrefset', '/usr/lib/python3.13/_weakrefset.py', 'PYMODULE'), + ('copyreg', '/usr/lib/python3.13/copyreg.py', 'PYMODULE'), + ('weakref', '/usr/lib/python3.13/weakref.py', 'PYMODULE'), ('encodings.zlib_codec', '/usr/lib/python3.13/encodings/zlib_codec.py', 'PYMODULE'), @@ -590,23 +612,10 @@ ('encodings.ascii', '/usr/lib/python3.13/encodings/ascii.py', 'PYMODULE'), ('encodings.aliases', '/usr/lib/python3.13/encodings/aliases.py', 'PYMODULE'), ('encodings', '/usr/lib/python3.13/encodings/__init__.py', 'PYMODULE'), - ('enum', '/usr/lib/python3.13/enum.py', 'PYMODULE'), - ('weakref', '/usr/lib/python3.13/weakref.py', 'PYMODULE'), - ('_weakrefset', '/usr/lib/python3.13/_weakrefset.py', 'PYMODULE'), - ('warnings', '/usr/lib/python3.13/warnings.py', 'PYMODULE'), - ('sre_constants', '/usr/lib/python3.13/sre_constants.py', 'PYMODULE'), - ('heapq', '/usr/lib/python3.13/heapq.py', 'PYMODULE'), - ('codecs', '/usr/lib/python3.13/codecs.py', 'PYMODULE'), ('traceback', '/usr/lib/python3.13/traceback.py', 'PYMODULE'), - ('linecache', '/usr/lib/python3.13/linecache.py', 'PYMODULE'), - ('re._parser', '/usr/lib/python3.13/re/_parser.py', 'PYMODULE'), - ('re._constants', '/usr/lib/python3.13/re/_constants.py', 'PYMODULE'), - ('re._compiler', '/usr/lib/python3.13/re/_compiler.py', 'PYMODULE'), - ('re._casefix', '/usr/lib/python3.13/re/_casefix.py', 'PYMODULE'), - ('re', '/usr/lib/python3.13/re/__init__.py', 'PYMODULE'), - ('posixpath', '/usr/lib/python3.13/posixpath.py', 'PYMODULE'), - ('reprlib', '/usr/lib/python3.13/reprlib.py', 'PYMODULE'), - ('abc', '/usr/lib/python3.13/abc.py', 'PYMODULE'), - ('collections', '/usr/lib/python3.13/collections/__init__.py', 'PYMODULE'), - ('ntpath', '/usr/lib/python3.13/ntpath.py', 'PYMODULE'), - ('stat', '/usr/lib/python3.13/stat.py', 'PYMODULE')]) + ('stat', '/usr/lib/python3.13/stat.py', 'PYMODULE'), + ('warnings', '/usr/lib/python3.13/warnings.py', 'PYMODULE'), + ('os', '/usr/lib/python3.13/os.py', 'PYMODULE'), + ('sre_compile', '/usr/lib/python3.13/sre_compile.py', 'PYMODULE'), + ('heapq', '/usr/lib/python3.13/heapq.py', 'PYMODULE'), + ('functools', '/usr/lib/python3.13/functools.py', 'PYMODULE')]) diff --git a/build/securecheck/EXE-00.toc b/build/securecheck/EXE-00.toc index a68cd49..5f2d196 100644 --- a/build/securecheck/EXE-00.toc +++ b/build/securecheck/EXE-00.toc @@ -90,8 +90,8 @@ ('python3.13/lib-dynload/_curses.cpython-313-x86_64-linux-gnu.so', '/usr/lib/python3.13/lib-dynload/_curses.cpython-313-x86_64-linux-gnu.so', 'EXTENSION'), - ('libz.so.1', '/lib/x86_64-linux-gnu/libz.so.1', 'BINARY'), ('libexpat.so.1', '/lib/x86_64-linux-gnu/libexpat.so.1', 'BINARY'), + ('libz.so.1', '/lib/x86_64-linux-gnu/libz.so.1', 'BINARY'), ('libcrypto.so.3', '/lib/x86_64-linux-gnu/libcrypto.so.3', 'BINARY'), ('libzstd.so.1', '/lib/x86_64-linux-gnu/libzstd.so.1', 'BINARY'), ('liblzma.so.5', '/lib/x86_64-linux-gnu/liblzma.so.5', 'BINARY'), @@ -114,17 +114,20 @@ ('securecheck/assets/securecheck-icon.svg', '/home/tuxgyver/scripts/securecheck/securecheck/assets/securecheck-icon.svg', 'DATA'), - ('wheel-0.46.1.dist-info/METADATA', - '/usr/lib/python3/dist-packages/wheel-0.46.1.dist-info/METADATA', + ('securecheck/assets/system_hardening.sh', + '/home/tuxgyver/scripts/securecheck/securecheck/assets/system_hardening.sh', 'DATA'), - ('wheel-0.46.1.dist-info/WHEEL', - '/usr/lib/python3/dist-packages/wheel-0.46.1.dist-info/WHEEL', + ('wheel-0.46.1.dist-info/entry_points.txt', + '/usr/lib/python3/dist-packages/wheel-0.46.1.dist-info/entry_points.txt', 'DATA'), ('wheel-0.46.1.dist-info/INSTALLER', '/usr/lib/python3/dist-packages/wheel-0.46.1.dist-info/INSTALLER', 'DATA'), - ('wheel-0.46.1.dist-info/entry_points.txt', - '/usr/lib/python3/dist-packages/wheel-0.46.1.dist-info/entry_points.txt', + ('wheel-0.46.1.dist-info/WHEEL', + '/usr/lib/python3/dist-packages/wheel-0.46.1.dist-info/WHEEL', + 'DATA'), + ('wheel-0.46.1.dist-info/METADATA', + '/usr/lib/python3/dist-packages/wheel-0.46.1.dist-info/METADATA', 'DATA'), ('base_library.zip', '/home/tuxgyver/scripts/securecheck/build/securecheck/base_library.zip', @@ -132,7 +135,7 @@ [], False, False, - 1775421404, + 1775457449, [('run', '/home/tuxgyver/.local/lib/python3.13/site-packages/PyInstaller/bootloader/Linux-64bit-intel/run', 'EXECUTABLE')], diff --git a/build/securecheck/PKG-00.toc b/build/securecheck/PKG-00.toc index 7d55f75..1debaaf 100644 --- a/build/securecheck/PKG-00.toc +++ b/build/securecheck/PKG-00.toc @@ -85,8 +85,8 @@ ('python3.13/lib-dynload/_curses.cpython-313-x86_64-linux-gnu.so', '/usr/lib/python3.13/lib-dynload/_curses.cpython-313-x86_64-linux-gnu.so', 'EXTENSION'), - ('libz.so.1', '/lib/x86_64-linux-gnu/libz.so.1', 'BINARY'), ('libexpat.so.1', '/lib/x86_64-linux-gnu/libexpat.so.1', 'BINARY'), + ('libz.so.1', '/lib/x86_64-linux-gnu/libz.so.1', 'BINARY'), ('libcrypto.so.3', '/lib/x86_64-linux-gnu/libcrypto.so.3', 'BINARY'), ('libzstd.so.1', '/lib/x86_64-linux-gnu/libzstd.so.1', 'BINARY'), ('liblzma.so.5', '/lib/x86_64-linux-gnu/liblzma.so.5', 'BINARY'), @@ -109,17 +109,20 @@ ('securecheck/assets/securecheck-icon.svg', '/home/tuxgyver/scripts/securecheck/securecheck/assets/securecheck-icon.svg', 'DATA'), - ('wheel-0.46.1.dist-info/METADATA', - '/usr/lib/python3/dist-packages/wheel-0.46.1.dist-info/METADATA', + ('securecheck/assets/system_hardening.sh', + '/home/tuxgyver/scripts/securecheck/securecheck/assets/system_hardening.sh', 'DATA'), - ('wheel-0.46.1.dist-info/WHEEL', - '/usr/lib/python3/dist-packages/wheel-0.46.1.dist-info/WHEEL', + ('wheel-0.46.1.dist-info/entry_points.txt', + '/usr/lib/python3/dist-packages/wheel-0.46.1.dist-info/entry_points.txt', 'DATA'), ('wheel-0.46.1.dist-info/INSTALLER', '/usr/lib/python3/dist-packages/wheel-0.46.1.dist-info/INSTALLER', 'DATA'), - ('wheel-0.46.1.dist-info/entry_points.txt', - '/usr/lib/python3/dist-packages/wheel-0.46.1.dist-info/entry_points.txt', + ('wheel-0.46.1.dist-info/WHEEL', + '/usr/lib/python3/dist-packages/wheel-0.46.1.dist-info/WHEEL', + 'DATA'), + ('wheel-0.46.1.dist-info/METADATA', + '/usr/lib/python3/dist-packages/wheel-0.46.1.dist-info/METADATA', 'DATA'), ('base_library.zip', '/home/tuxgyver/scripts/securecheck/build/securecheck/base_library.zip', diff --git a/build/securecheck/PYZ-00.pyz b/build/securecheck/PYZ-00.pyz index fe0a13d..343eee4 100644 Binary files a/build/securecheck/PYZ-00.pyz and b/build/securecheck/PYZ-00.pyz differ diff --git a/build/securecheck/PYZ-00.toc b/build/securecheck/PYZ-00.toc index 31794e4..94abef5 100644 --- a/build/securecheck/PYZ-00.toc +++ b/build/securecheck/PYZ-00.toc @@ -178,6 +178,9 @@ ('securecheck.storage', '/home/tuxgyver/scripts/securecheck/securecheck/storage.py', 'PYMODULE'), + ('securecheck.summary_utils', + '/home/tuxgyver/scripts/securecheck/securecheck/summary_utils.py', + 'PYMODULE'), ('securecheck.system_info', '/home/tuxgyver/scripts/securecheck/securecheck/system_info.py', 'PYMODULE'), diff --git a/build/securecheck/base_library.zip b/build/securecheck/base_library.zip index 551b74c..cb0164b 100644 Binary files a/build/securecheck/base_library.zip and b/build/securecheck/base_library.zip differ diff --git a/build/securecheck/securecheck.pkg b/build/securecheck/securecheck.pkg index a7c590d..52462ce 100644 Binary files a/build/securecheck/securecheck.pkg and b/build/securecheck/securecheck.pkg differ diff --git a/build/securecheck/warn-securecheck.txt b/build/securecheck/warn-securecheck.txt index 5073300..aa63232 100644 --- a/build/securecheck/warn-securecheck.txt +++ b/build/securecheck/warn-securecheck.txt @@ -18,8 +18,8 @@ missing module named _frozen_importlib_external - imported by importlib._bootstr excluded module named _frozen_importlib - imported by importlib (optional), importlib.abc (optional), zipimport (top-level) missing module named 'collections.abc' - imported by traceback (top-level), typing (top-level), inspect (top-level), logging (top-level), importlib.resources.readers (top-level), selectors (top-level), tracemalloc (top-level), http.client (top-level), pkg_resources (top-level), setuptools (top-level), setuptools._distutils.filelist (top-level), setuptools._distutils.util (top-level), jaraco.functools (top-level), more_itertools.more (top-level), more_itertools.recipes (top-level), setuptools._distutils._modified (top-level), setuptools._distutils.compat (top-level), setuptools._distutils.spawn (top-level), typing_extensions (top-level), asyncio.base_events (top-level), asyncio.coroutines (top-level), setuptools._distutils.compilers.C.base (top-level), setuptools._distutils.fancy_getopt (top-level), setuptools._reqs (top-level), setuptools.discovery (top-level), setuptools.dist (top-level), setuptools._distutils.command.bdist (top-level), setuptools._distutils.core (top-level), setuptools._distutils.cmd (top-level), setuptools._distutils.dist (top-level), configparser (top-level), setuptools._distutils.extension (top-level), setuptools.config.setupcfg (top-level), setuptools.config.expand (top-level), setuptools.config.pyprojecttoml (top-level), setuptools.config._apply_pyprojecttoml (top-level), tomllib._parser (top-level), setuptools._vendor.tomli._parser (top-level), setuptools.command.egg_info (top-level), setuptools._distutils.command.build (top-level), setuptools._distutils.command.sdist (top-level), setuptools.glob (top-level), setuptools.command._requirestxt (top-level), setuptools.command.bdist_wheel (top-level), platformdirs.api (conditional), platformdirs.windows (conditional), platformdirs.unix (conditional), setuptools._distutils.command.build_ext (top-level), _pyrepl.types (top-level), _pyrepl.readline (top-level), setuptools._distutils.compilers.C.msvc (top-level) missing module named winreg - imported by importlib._bootstrap_external (conditional), mimetypes (optional), urllib.request (delayed, conditional, optional), platform (delayed, optional), platformdirs.windows (delayed, optional), setuptools._distutils.compilers.C.msvc (top-level), setuptools.msvc (conditional) -missing module named nt - imported by shutil (conditional), importlib._bootstrap_external (conditional), _colorize (delayed, conditional, optional), os (delayed, conditional, optional), ntpath (optional), ctypes (delayed, conditional), _pyrepl.windows_console (delayed, optional) -missing module named _winapi - imported by encodings (delayed, conditional, optional), shutil (conditional), subprocess (conditional), ntpath (optional), mimetypes (optional), sysconfig (delayed), multiprocessing.connection (optional), multiprocessing.spawn (delayed, conditional), multiprocessing.reduction (conditional), multiprocessing.shared_memory (conditional), multiprocessing.heap (conditional), multiprocessing.popen_spawn_win32 (top-level), asyncio.windows_events (top-level), asyncio.windows_utils (top-level) +missing module named nt - imported by shutil (conditional), importlib._bootstrap_external (conditional), ntpath (optional), _colorize (delayed, conditional, optional), os (delayed, conditional, optional), ctypes (delayed, conditional), _pyrepl.windows_console (delayed, optional) +missing module named _winapi - imported by encodings (delayed, conditional, optional), shutil (conditional), ntpath (optional), subprocess (conditional), mimetypes (optional), sysconfig (delayed), multiprocessing.connection (optional), multiprocessing.spawn (delayed, conditional), multiprocessing.reduction (conditional), multiprocessing.shared_memory (conditional), multiprocessing.heap (conditional), multiprocessing.popen_spawn_win32 (top-level), asyncio.windows_events (top-level), asyncio.windows_utils (top-level) missing module named msvcrt - imported by subprocess (optional), getpass (optional), multiprocessing.spawn (delayed, conditional), multiprocessing.popen_spawn_win32 (top-level), asyncio.windows_events (top-level), asyncio.windows_utils (top-level), _pyrepl.windows_console (top-level) missing module named win32evtlog - imported by logging.handlers (delayed, optional) missing module named win32evtlogutil - imported by logging.handlers (delayed, optional) diff --git a/build/securecheck/xref-securecheck.html b/build/securecheck/xref-securecheck.html index 4994f80..08dbca9 100644 --- a/build/securecheck/xref-securecheck.html +++ b/build/securecheck/xref-securecheck.html @@ -352,6 +352,7 @@ imported by: • securecheck.modelssecurecheck.statussecurecheck.storage + • securecheck.summary_utilssecurecheck.system_infosecurecheck.taskssetuptools @@ -10444,9 +10445,12 @@ imported by: • randomrun.pyrunpy + • securecheck.__main__securecheck.configsecurecheck.executor + • securecheck.logging_utilssecurecheck.system_info + • securecheck.taskssetuptoolssetuptools._core_metadatasetuptools._distutils.archive_util @@ -11840,7 +11844,7 @@ imported by: • re._parserrlcompleterrun.py - • securecheck.app + • securecheck.summary_utilssecurecheck.taskssetuptoolssetuptools._distutils.cmd @@ -12115,6 +12119,7 @@ imported by: • securecheck.modelssecurecheck.statussecurecheck.storage + • securecheck.summary_utilssecurecheck.system_infosecurecheck.tasks @@ -12130,6 +12135,7 @@ imports: __future__argparsedatetime + • ossecurechecksecurecheck.appsecurecheck.catalog @@ -12159,12 +12165,12 @@ imports: • collectionscursesdataclasses - • resecurechecksecurecheck.assetssecurecheck.modelssecurecheck.statussecurecheck.storage + • securecheck.summary_utilssecurecheck.system_infotextwraptyping @@ -12280,6 +12286,7 @@ imports: __future__logginglogging.handlers + • ospathlibsecurecheck @@ -12311,6 +12318,7 @@ imported by: • securecheck.catalogsecurecheck.executorsecurecheck.storage + • securecheck.summary_utilssecurecheck.tasks @@ -12361,6 +12369,26 @@ imported by: +
+ + securecheck.summary_utils +SourceModule
+imports: + __future__ + • re + • securecheck + • securecheck.models + • typing + +
+
+imported by: + securecheck.app + +
+ +
+
securecheck.system_info @@ -12394,12 +12422,15 @@ imports: __future__datetimejson + • ospathlibresecurechecksecurecheck.assetssecurecheck.executorsecurecheck.models + • stat + • tempfile
@@ -16098,6 +16129,7 @@ imported by: • posixpathrun.pysecurecheck.executor + • securecheck.taskssetuptools._core_metadatasetuptools._distutils.file_utilsetuptools._shutil @@ -16612,6 +16644,7 @@ imported by: • pkg_resourcessecurecheck.configsecurecheck.executor + • securecheck.taskssetuptools._core_metadatasetuptools._distutils.compilers.C.basesetuptools._distutils.util @@ -17166,6 +17199,7 @@ imported by: • platformdirs.windowssecurecheck.appsecurecheck.models + • securecheck.summary_utilssetuptoolssetuptools._distutils._modifiedsetuptools._distutils.archive_util diff --git a/dist/securecheck b/dist/securecheck index 19a405d..4db56fb 100755 Binary files a/dist/securecheck and b/dist/securecheck differ diff --git a/securecheck/__main__.py b/securecheck/__main__.py index 6381c79..ad0fa61 100644 --- a/securecheck/__main__.py +++ b/securecheck/__main__.py @@ -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 diff --git a/securecheck/__pycache__/__main__.cpython-313.pyc b/securecheck/__pycache__/__main__.cpython-313.pyc index bafd762..84575db 100644 Binary files a/securecheck/__pycache__/__main__.cpython-313.pyc and b/securecheck/__pycache__/__main__.cpython-313.pyc differ diff --git a/securecheck/__pycache__/app.cpython-313.pyc b/securecheck/__pycache__/app.cpython-313.pyc index 46e2c56..af5bfcf 100644 Binary files a/securecheck/__pycache__/app.cpython-313.pyc and b/securecheck/__pycache__/app.cpython-313.pyc differ diff --git a/securecheck/__pycache__/catalog.cpython-313.pyc b/securecheck/__pycache__/catalog.cpython-313.pyc index 4dce677..7ff10dd 100644 Binary files a/securecheck/__pycache__/catalog.cpython-313.pyc and b/securecheck/__pycache__/catalog.cpython-313.pyc differ diff --git a/securecheck/__pycache__/logging_utils.cpython-313.pyc b/securecheck/__pycache__/logging_utils.cpython-313.pyc index 145884b..9fa539d 100644 Binary files a/securecheck/__pycache__/logging_utils.cpython-313.pyc and b/securecheck/__pycache__/logging_utils.cpython-313.pyc differ diff --git a/securecheck/__pycache__/status.cpython-313.pyc b/securecheck/__pycache__/status.cpython-313.pyc index fecb2b3..876d983 100644 Binary files a/securecheck/__pycache__/status.cpython-313.pyc and b/securecheck/__pycache__/status.cpython-313.pyc differ diff --git a/securecheck/__pycache__/summary_utils.cpython-313.pyc b/securecheck/__pycache__/summary_utils.cpython-313.pyc new file mode 100644 index 0000000..91cb81a Binary files /dev/null and b/securecheck/__pycache__/summary_utils.cpython-313.pyc differ diff --git a/securecheck/__pycache__/tasks.cpython-313.pyc b/securecheck/__pycache__/tasks.cpython-313.pyc index e2879dc..769a24e 100644 Binary files a/securecheck/__pycache__/tasks.cpython-313.pyc and b/securecheck/__pycache__/tasks.cpython-313.pyc differ diff --git a/securecheck/app.py b/securecheck/app.py index 9ff6b89..eeb0aec 100644 --- a/securecheck/app.py +++ b/securecheck/app.py @@ -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: diff --git a/securecheck/assets/system_hardening.sh b/securecheck/assets/system_hardening.sh new file mode 100644 index 0000000..84726f5 --- /dev/null +++ b/securecheck/assets/system_hardening.sh @@ -0,0 +1,1154 @@ +#!/bin/bash + +################################################################################ +# Script: system_hardening_optimized.sh +# Version: 8.2 +# Description: Système de durcissement sécurité pour Debian/Ubuntu LTS +# Mode autonome avec valeurs par défaut sécurisées +# License: GPLv3 +################################################################################ + +set -euo pipefail + +readonly LOG_FILE="/var/log/system_hardening.log" +readonly STATUS_FILE="/var/log/hardening_status.log" +readonly BACKUP_DIR="/root/backup_hardening_$(date +%Y%m%d_%H%M%S)" +readonly SECURITY_REPORT="/var/log/security_report_$(date +%Y%m%d).log" +readonly OPEN_PORTS_FILE="/tmp/open_ports_detected.txt" + +readonly DEFAULT_SSH_PORT=22022 +readonly DEFAULT_TIMEZONE="Europe/Paris" +readonly DEFAULT_PASS_MAX_DAYS=90 +readonly DEFAULT_PASS_MIN_DAYS=7 +readonly DEFAULT_UMASK="027" + +: "${AUTO_SSH_PORT:=$DEFAULT_SSH_PORT}" +: "${AUTO_TIMEZONE:=$DEFAULT_TIMEZONE}" +: "${AUTO_PASS_MAX_DAYS:=$DEFAULT_PASS_MAX_DAYS}" +: "${AUTO_PASS_MIN_DAYS:=$DEFAULT_PASS_MIN_DAYS}" +: "${AUTO_UMASK:=$DEFAULT_UMASK}" +: "${AUTO_DISABLE_ROOT_LOGIN:=no}" +: "${AUTO_ENABLE_FAIL2BAN:=yes}" +: "${AUTO_ENABLE_UFW:=yes}" +: "${AUTO_ENABLE_AIDE:=yes}" +: "${AUTO_ENABLE_CLAMAV:=yes}" +: "${AUTO_SKIP_PORTS_DETECTION:=no}" +: "${AUTO_SKIP_LYNIS:=no}" +: "${AUTO_YES:=no}" +: "${AUTO_CLEANUP_SSH:=no}" +: "${AUTO_CHANGE_ROOT_PWD:=no}" +: "${AUTO_SKIP_DEBSUMS_CONTAINER:=yes}" + +TOTAL_STEPS=33 +CURRENT_STEP=1 + +FORCE_ALL=false +FORCE_STEPS=() +SKIP_STEPS=() +RESET_ALL=false +LIST_STEPS=false +SHOW_STATUS=false +UNATTENDED=false + +readonly RED='\033[0;31m' +readonly GREEN='\033[0;32m' +readonly YELLOW='\033[1;33m' +readonly CYAN='\033[0;36m' +readonly BLUE='\033[0;34m' +readonly MAGENTA='\033[0;35m' +readonly NC='\033[0m' + +declare -A STEP_DESCRIPTIONS=( + ["install_security_tools"]="Installation des outils de sécurité" + ["change_root_password"]="Changement mot de passe root aléatoire" + ["detect_open_ports"]="Détection des ports ouverts" + ["configure_process_accounting"]="Configuration Process Accounting" + ["configure_sysctl_security"]="Durcissement sysctl" + ["configure_log_permissions"]="Permissions des logs" + ["configure_pam_password_policy"]="Politique mots de passe PAM" + ["configure_login_defs"]="Configuration login.defs" + ["configure_umask"]="Configuration umask" + ["configure_aide_sha512"]="Configuration AIDE SHA512" + ["initialize_aide_db"]="Initialisation base AIDE" + ["configure_clamav"]="Configuration ClamAV" + ["configure_chrony"]="Configuration Chrony" + ["harden_ssh"]="Durcissement SSH" + ["configure_banners"]="Configuration bannières" + ["configure_firewall_ports"]="Configuration pare-feu UFW" + ["configure_fail2ban"]="Configuration Fail2ban" + ["remove_unneeded_packages"]="Suppression paquets inutiles" + ["restrict_file_permissions"]="Restriction permissions fichiers" + ["disable_risky_kernel_modules"]="Désactivation modules noyau" + ["configure_security_limits"]="Configuration limites sécurité" + ["verify_packages_integrity"]="Vérification intégrité paquets" + ["configure_automatic_updates"]="Configuration mises à jour auto" + ["configure_aide_cron"]="Configuration tâche AIDE cron" + ["harden_smtp_banner"]="Durcissement bannière SMTP" + ["harden_systemd_services"]="Durcissement services systemd" + ["configure_advanced_pam"]="Configuration PAM avancée" + ["check_partition_layout"]="Vérification partitions" + ["check_vmlinuz"]="Vérification fichiers noyau" + ["run_chkrootkit"]="Exécution chkrootkit" + ["prepare_ssh_cleanup"]="Préparation nettoyage SSH" + ["run_lynis_audit"]="Audit Lynis final" +) + +log_message() { + local message="$1" + local level="${2:-INFO}" + echo -e "[$(date +'%Y-%m-%d %H:%M:%S')] [$level] $message" | tee -a "$LOG_FILE" +} + +print_step() { + local step_title="$1" + echo -e "\n${YELLOW}[STEP ${CURRENT_STEP}/${TOTAL_STEPS}] $step_title${NC}" + log_message "Début étape $CURRENT_STEP: $step_title" "STEP" + CURRENT_STEP=$((CURRENT_STEP + 1)) +} + +print_success() { + echo -e "${GREEN}[OK]${NC} $1" + log_message "$1" "SUCCESS" +} + +print_warning() { + echo -e "${YELLOW}[WARN]${NC} $1" + log_message "$1" "WARNING" +} + +print_error() { + echo -e "${RED}[ERR]${NC} $1" >&2 + log_message "$1" "ERROR" +} + +print_info() { + echo -e "${BLUE}[INFO]${NC} $1" + log_message "$1" "INFO" +} + +auto_confirm() { + [[ "$AUTO_YES" == "yes" ]] && return 0 + [[ "$UNATTENDED" == true ]] && return 0 + return 1 +} + +check_step_done() { + local step_name="$1" + $FORCE_ALL && return 1 + [[ " ${FORCE_STEPS[@]} " =~ " ${step_name} " ]] && { print_info "Forçage de l'étape: $step_name"; return 1; } + [[ " ${SKIP_STEPS[@]} " =~ " ${step_name} " ]] && { print_info "Saut de l'étape: $step_name"; return 0; } + grep -q "^${step_name}$" "$STATUS_FILE" 2>/dev/null +} + +mark_step_done() { + local step_name="$1" + [[ " ${SKIP_STEPS[@]} " =~ " ${step_name} " ]] && { print_info "Étape $step_name ignorée"; return 0; } + sed -i "/^${step_name}$/d" "$STATUS_FILE" 2>/dev/null || true + echo "$step_name" >> "$STATUS_FILE" + log_message "Étape '$step_name' terminée" "STATUS" +} + +skip_step() { + local step_name="$1" + print_info "Étape déjà effectuée : $step_name" + CURRENT_STEP=$((CURRENT_STEP + 1)) +} + +detect_container() { + [[ -f /.dockerenv ]] && return 0 + [[ -d /dev/lxc ]] && return 0 + [[ -f /.lxcpid ]] && return 0 + [[ -n "${container:-}" ]] && return 0 + [[ -n "${DOCKER_HOST:-}" ]] && return 0 + [[ -n "${KUBERNETES_SERVICE_HOST:-}" ]] && return 0 + if [[ -f /proc/1/cgroup ]]; then + local cgroup_content + cgroup_content=$(cat /proc/1/cgroup 2>/dev/null || echo "") + echo "$cgroup_content" | grep -qE "(docker|lxc|kubepods|containerd|podman)" && return 0 + fi + if command -v systemd-detect-virt >/dev/null 2>&1; then + local virt + virt=$(systemd-detect-virt 2>/dev/null) + case "$virt" in + "container"|"lxc"|"lxc-libvirt"|"systemd-nspawn"|"docker"|"podman"|"wsl") return 0 ;; + esac + fi + [[ -f /run/.containerenv ]] && return 0 + log_message "Aucun conteneur détecté" "DETECT" + return 1 +} + +detect_lxc() { + [[ -d /dev/lxc ]] && return 0 + [[ -f /.lxcpid ]] && return 0 + grep -q "lxc" /proc/1/cgroup 2>/dev/null && return 0 + [[ "$(systemd-detect-virt 2>/dev/null)" == "lxc" ]] && return 0 + return 1 +} + +backup_file() { + local file_path="$1" + [[ -f "$file_path" ]] && { + mkdir -p "$BACKUP_DIR" + cp "$file_path" "${BACKUP_DIR}/" + log_message "Sauvegarde de $file_path" "BACKUP" + } +} + +add_unique_line() { + local line="$1" + local file="$2" + grep -qF "$line" "$file" 2>/dev/null || { + echo "$line" >> "$file" + log_message "Ajout ligne: '$line' -> $file" "CONFIG" + } +} + +update_config_value() { + local file="$1" + local key="$2" + local value="$3" + backup_file "$file" + if grep -q "^${key}" "$file"; then + sed -i "s/^${key}.*/${key} ${value}/" "$file" + else + echo "${key} ${value}" >> "$file" + fi +} + +detect_open_ports() { + local step_name="detect_open_ports" + [[ "$AUTO_SKIP_PORTS_DETECTION" == "yes" ]] && { mark_step_done "$step_name"; return 0; } + check_step_done "$step_name" && { skip_step "${STEP_DESCRIPTIONS[$step_name]}"; return 0; } + print_step "Détection des ports ouverts" + if command -v ss > /dev/null 2>&1; then + ss -tlnp | grep LISTEN | awk '{print $4}' | awk -F: '{print $NF}' | sort -n | uniq > "$OPEN_PORTS_FILE" 2>/dev/null + elif command -v netstat > /dev/null 2>&1; then + netstat -tlnp 2>/dev/null | grep LISTEN | awk '{print $4}' | awk -F: '{print $NF}' | sort -n | uniq > "$OPEN_PORTS_FILE" + else + echo "22" > "$OPEN_PORTS_FILE" + fi + local port_count=$(wc -l < "$OPEN_PORTS_FILE" 2>/dev/null || echo 0) + print_success "$port_count port(s) ouvert(s) détecté(s)" + mark_step_done "$step_name" +} + +is_port_open() { + grep -q "^${1}$" "$OPEN_PORTS_FILE" 2>/dev/null +} + +get_ssh_port_to_use() { + detect_lxc && echo "22" || echo "$AUTO_SSH_PORT" +} + +configure_banners() { + local step_name="configure_banners" + check_step_done "$step_name" && { skip_step "${STEP_DESCRIPTIONS[$step_name]}"; return 0; } + print_step "Configuration des bannières légales" + cat > /etc/issue.net << 'EOF' +╔══════════════════════════════════════════════════════════════════╗ +║ SYSTÈME SÉCURISÉ ║ +║ Accès strictement réservé aux personnes autorisées. ║ +║ Toutes les activités sont surveillées et enregistrées. ║ +╚══════════════════════════════════════════════════════════════════╝ +EOF + chmod 644 /etc/issue.net + cat > /etc/issue << 'EOF' +╔══════════════════════════════════════════════════════════════════╗ +║ SYSTÈME SÉCURISÉ ║ +║ Accès strictement réservé aux personnes autorisées. ║ +╚══════════════════════════════════════════════════════════════════╝ +EOF + chmod 644 /etc/issue + [[ -d /etc/update-motd.d ]] && chmod -x /etc/update-motd.d/* 2>/dev/null || true + print_success "Bannières légales configurées" + mark_step_done "$step_name" +} + +_filter_available_packages() { + local available=() + for pkg in "$@"; do + if apt-cache show "$pkg" >/dev/null 2>&1; then + available+=("$pkg") + else + log_message "Paquet non disponible sur ce système, ignoré: $pkg" "WARNING" + fi + done + echo "${available[@]:-}" +} + +install_security_tools() { + local step_name="install_security_tools" + check_step_done "$step_name" && { skip_step "${STEP_DESCRIPTIONS[$step_name]}"; return 0; } + print_step "Mise à jour système et installation outils de sécurité" + DEBIAN_FRONTEND=noninteractive apt-get update -qq + DEBIAN_FRONTEND=noninteractive apt-get upgrade -y -qq + local candidate_packages=( + lynis aide aide-common ufw libpam-pwquality + apt-listchanges needrestart chrony + chkrootkit libpam-tmpdir debsums unattended-upgrades + ) + [[ "$AUTO_ENABLE_FAIL2BAN" == "yes" ]] && candidate_packages+=(fail2ban) + [[ "$AUTO_ENABLE_CLAMAV" == "yes" ]] && candidate_packages+=(clamav clamav-daemon) + detect_container || candidate_packages+=(acct) + local installable + installable=$(_filter_available_packages "${candidate_packages[@]}") + if [[ -n "$installable" ]]; then + # shellcheck disable=SC2086 + DEBIAN_FRONTEND=noninteractive apt-get install -y -qq $installable + fi + print_success "Système mis à jour et outils installés" + mark_step_done "$step_name" +} + +change_root_password() { + local step_name="change_root_password" + check_step_done "$step_name" && { skip_step "${STEP_DESCRIPTIONS[$step_name]}"; return 0; } + print_step "Changement du mot de passe root" + if [[ "$AUTO_CHANGE_ROOT_PWD" != "yes" ]]; then + print_info "Changement mot de passe root désactivé (AUTO_CHANGE_ROOT_PWD=no)" + mark_step_done "$step_name" + return 0 + fi + detect_container && { print_warning "Conteneur détecté - changement ignoré"; mark_step_done "$step_name"; return 0; } + local root_shell=$(grep "^root:" /etc/passwd | cut -d: -f7) + [[ "$root_shell" == "/usr/sbin/nologin" || "$root_shell" == "/bin/false" ]] && { + print_warning "Root a un shell non interactif - ignoré" + mark_step_done "$step_name" + return 0 + } + local NEW_ROOT_PASSWORD + NEW_ROOT_PASSWORD=$(tr -dc 'A-Za-z0-9!@#$%^&*' < /dev/urandom | head -c 20) + local PWD_FILE="/root/root_password_$(date +%Y%m%d_%H%M%S).txt" + cat > "$PWD_FILE" << EOF +=== MOT DE PASSE ROOT - $(hostname) - $(date) === +Mot de passe root: $NEW_ROOT_PASSWORD +CONSERVEZ CE FICHIER EN LIEU SÛR +=== FIN === +EOF + chmod 600 "$PWD_FILE" + chown root:root "$PWD_FILE" + if echo "root:$NEW_ROOT_PASSWORD" | chpasswd -c SHA512 2>/dev/null; then + chage -M 90 root 2>/dev/null || true + print_success "Mot de passe root changé - sauvegardé dans $PWD_FILE" + else + rm -f "$PWD_FILE" + print_error "Échec changement mot de passe root" + return 1 + fi + mark_step_done "$step_name" +} + +configure_process_accounting() { + local step_name="configure_process_accounting" + check_step_done "$step_name" && { skip_step "${STEP_DESCRIPTIONS[$step_name]}"; return 0; } + print_step "Configuration du Process Accounting" + if detect_container; then + print_warning "Conteneur détecté - Process Accounting désactivé" + for service in acct.service psacct.service; do + systemctl list-unit-files | grep -q "^${service}" && { + systemctl stop "$service" 2>/dev/null || true + systemctl disable "$service" 2>/dev/null || true + systemctl mask "$service" 2>/dev/null || true + } + done + else + systemctl enable acct.service 2>/dev/null && \ + systemctl start acct.service 2>/dev/null && \ + print_success "Process Accounting activé" || \ + print_warning "Process Accounting non disponible" + fi + mark_step_done "$step_name" +} + +configure_sysctl_security() { + local step_name="configure_sysctl_security" + check_step_done "$step_name" && { skip_step "${STEP_DESCRIPTIONS[$step_name]}"; return 0; } + print_step "Durcissement des paramètres noyau (sysctl)" + cat > /etc/sysctl.d/99-security-hardening.conf << 'EOF' +net.ipv4.conf.all.accept_source_route = 0 +net.ipv4.conf.all.accept_redirects = 0 +net.ipv4.conf.all.send_redirects = 0 +net.ipv4.conf.all.log_martians = 1 +net.ipv4.conf.all.rp_filter = 1 +net.ipv4.conf.default.accept_source_route = 0 +net.ipv4.conf.default.accept_redirects = 0 +net.ipv4.conf.default.log_martians = 1 +net.ipv4.icmp_echo_ignore_broadcasts = 1 +net.ipv4.icmp_ignore_bogus_error_responses = 1 +net.ipv4.tcp_syncookies = 1 +net.ipv4.tcp_max_syn_backlog = 2048 +net.ipv4.tcp_synack_retries = 2 +net.ipv4.tcp_syn_retries = 5 +net.ipv6.conf.all.accept_redirects = 0 +net.ipv6.conf.all.accept_source_route = 0 +net.ipv6.conf.default.accept_redirects = 0 +net.ipv6.conf.default.accept_source_route = 0 +kernel.randomize_va_space = 2 +kernel.kptr_restrict = 2 +kernel.dmesg_restrict = 1 +kernel.yama.ptrace_scope = 1 +kernel.unprivileged_bpf_disabled = 1 +fs.suid_dumpable = 0 +fs.protected_fifos = 2 +fs.protected_regular = 2 +fs.protected_symlinks = 1 +fs.protected_hardlinks = 1 +EOF + sysctl -p /etc/sysctl.d/99-security-hardening.conf 2>/dev/null || print_warning "Certains paramètres sysctl ignorés" + print_success "Paramètres sysctl appliqués" + mark_step_done "$step_name" +} + +configure_log_permissions() { + local step_name="configure_log_permissions" + check_step_done "$step_name" && { skip_step "${STEP_DESCRIPTIONS[$step_name]}"; return 0; } + print_step "Configuration des permissions des fichiers de log" + chmod 750 /var/log 2>/dev/null || true + chown root:adm /var/log 2>/dev/null || true + find /var/log -type f -exec chmod 640 {} \; 2>/dev/null || true + for log_file in /var/log/auth.log /var/log/syslog /var/log/secure /var/log/messages; do + [[ -f "$log_file" ]] && chmod 600 "$log_file" 2>/dev/null || true + done + [[ -d /var/log/audit ]] && { chmod 700 /var/log/audit 2>/dev/null || true; find /var/log/audit -type f -exec chmod 600 {} \; 2>/dev/null || true; } + command -v journalctl > /dev/null 2>&1 && { chmod 2750 /var/log/journal 2>/dev/null || true; chown root:systemd-journal /var/log/journal 2>/dev/null || true; } + print_success "Permissions des logs configurées" + mark_step_done "$step_name" +} + +configure_pam_password_policy() { + local step_name="configure_pam_password_policy" + check_step_done "$step_name" && { skip_step "${STEP_DESCRIPTIONS[$step_name]}"; return 0; } + print_step "Configuration de la politique de mots de passe PAM" + backup_file "/etc/security/pwquality.conf" + cat > /etc/security/pwquality.conf << 'EOF' +minlen = 14 +minclass = 3 +maxrepeat = 3 +maxsequence = 3 +dcredit = -1 +ucredit = -1 +lcredit = -1 +ocredit = -1 +difok = 3 +maxclassrepeat = 3 +gecoscheck = 1 +EOF + backup_file "/etc/pam.d/common-password" + if grep -q "pam_pwquality.so" /etc/pam.d/common-password; then + sed -i 's/pam_pwquality.so.*/pam_pwquality.so retry=3 minlen=14 minclass=3 ucredit=-1 lcredit=-1 dcredit=-1 ocredit=-1 enforce_for_root/' /etc/pam.d/common-password + else + sed -i '/^password.*pam_unix.so/ipassword requisite pam_pwquality.so retry=3 minlen=14 minclass=3 ucredit=-1 lcredit=-1 dcredit=-1 ocredit=-1 enforce_for_root' /etc/pam.d/common-password + fi + sed -i 's/pam_unix.so.*/& sha512 rounds=500000 remember=5/' /etc/pam.d/common-password + print_success "Politique PAM configurée" + mark_step_done "$step_name" +} + +configure_login_defs() { + local step_name="configure_login_defs" + check_step_done "$step_name" && { skip_step "${STEP_DESCRIPTIONS[$step_name]}"; return 0; } + print_step "Configuration des paramètres de connexion (login.defs)" + backup_file "/etc/login.defs" + update_config_value "/etc/login.defs" "PASS_MAX_DAYS" "$AUTO_PASS_MAX_DAYS" + update_config_value "/etc/login.defs" "PASS_MIN_DAYS" "$AUTO_PASS_MIN_DAYS" + update_config_value "/etc/login.defs" "PASS_WARN_AGE" "7" + update_config_value "/etc/login.defs" "LOGIN_RETRIES" "3" + update_config_value "/etc/login.defs" "LOGIN_TIMEOUT" "60" + update_config_value "/etc/login.defs" "UMASK" "$AUTO_UMASK" + update_config_value "/etc/login.defs" "ENCRYPT_METHOD" "SHA512" + update_config_value "/etc/login.defs" "SHA_CRYPT_MIN_ROUNDS" "500000" + update_config_value "/etc/login.defs" "SHA_CRYPT_MAX_ROUNDS" "1000000" + print_success "Configuration login.defs appliquée" + mark_step_done "$step_name" +} + +configure_umask() { + local step_name="configure_umask" + check_step_done "$step_name" && { skip_step "${STEP_DESCRIPTIONS[$step_name]}"; return 0; } + print_step "Configuration de l'umask par défaut" + for file in /etc/profile /etc/bash.bashrc /etc/zsh/zshrc; do + [[ -f "$file" ]] && { backup_file "$file"; sed -i '/^umask/d' "$file"; add_unique_line "umask $AUTO_UMASK" "$file"; } + done + echo "umask $AUTO_UMASK" > /etc/profile.d/umask.sh + chmod 644 /etc/profile.d/umask.sh + print_success "Umask configuré à $AUTO_UMASK" + mark_step_done "$step_name" +} + +configure_aide_sha512() { + local step_name="configure_aide_sha512" + check_step_done "$step_name" && { skip_step "${STEP_DESCRIPTIONS[$step_name]}"; return 0; } + [[ "$AUTO_ENABLE_AIDE" != "yes" ]] && { mark_step_done "$step_name"; return 0; } + print_step "Configuration AIDE pour SHA512" + backup_file "/etc/aide/aide.conf" + cat > /etc/aide/aide.conf << 'EOF' +database=file:/var/lib/aide/aide.db.gz +database_out=file:/var/lib/aide/aide.db.new.gz +gzip_dbout=yes + +Normal = sha512 +Large = sha512+ftype +Everything = sha512+ftype+p+i+n+u+g+s+m+c+acl+selinux+xattrs + +/etc Everything +/bin Large +/sbin Large +/usr/bin Large +/usr/sbin Large +/boot Large +/lib Large +/lib64 Large +/root Large + +!/proc +!/sys +!/tmp +!/var/tmp +!/var/run +!/var/log +!/dev +EOF + print_success "AIDE configuré pour SHA512" + mark_step_done "$step_name" +} + +initialize_aide_db() { + local step_name="initialize_aide_db" + check_step_done "$step_name" && { skip_step "${STEP_DESCRIPTIONS[$step_name]}"; return 0; } + [[ "$AUTO_ENABLE_AIDE" != "yes" ]] && { mark_step_done "$step_name"; return 0; } + print_step "Initialisation de la base de données AIDE" + + # Corriger la config AIDE si elle utilise encore les macros @@{} non supportées + if grep -q '@@{' /etc/aide/aide.conf 2>/dev/null; then + print_warning "Config AIDE avec macros non supportées détectée - correction automatique" + cat > /etc/aide/aide.conf << 'EOF' +database=file:/var/lib/aide/aide.db.gz +database_out=file:/var/lib/aide/aide.db.new.gz +gzip_dbout=yes + +Normal = sha512 +Large = sha512+ftype +Everything = sha512+ftype+p+i+n+u+g+s+m+c+acl+selinux+xattrs + +/etc Everything +/bin Large +/sbin Large +/usr/bin Large +/usr/sbin Large +/boot Large +/lib Large +/lib64 Large +/root Large + +!/proc +!/sys +!/tmp +!/var/tmp +!/var/run +!/var/log +!/dev +EOF + fi + + if aide --init --config /etc/aide/aide.conf 2>&1 | tee -a "$LOG_FILE"; then + [[ -f /var/lib/aide/aide.db.new.gz ]] && mv /var/lib/aide/aide.db.new.gz /var/lib/aide/aide.db.gz + print_success "Base de données AIDE initialisée" + else + print_warning "Échec initialisation AIDE - étape ignorée, durcissement continue" + fi + mark_step_done "$step_name" +} + +configure_clamav() { + local step_name="configure_clamav" + check_step_done "$step_name" && { skip_step "${STEP_DESCRIPTIONS[$step_name]}"; return 0; } + [[ "$AUTO_ENABLE_CLAMAV" != "yes" ]] && { mark_step_done "$step_name"; return 0; } + print_step "Configuration de ClamAV" + mkdir -p /var/log/clamav /var/lib/clamav /run/clamav + id -u clamav >/dev/null 2>&1 || useradd -r -M -d /var/lib/clamav -s /bin/false -c "Clam AntiVirus" clamav 2>/dev/null || true + chown -R clamav:clamav /var/log/clamav /var/lib/clamav /run/clamav 2>/dev/null || true + touch /var/log/clamav/freshclam.log /var/log/clamav/clamav.log 2>/dev/null || true + chown clamav:clamav /var/log/clamav/freshclam.log /var/log/clamav/clamav.log 2>/dev/null || true + systemctl stop clamav-freshclam clamav-daemon 2>/dev/null || true + sleep 2 + su - clamav -s /bin/bash -c "freshclam --quiet" 2>&1 | tee -a "$LOG_FILE" || \ + freshclam --user=clamav --quiet 2>&1 | tee -a "$LOG_FILE" || \ + print_warning "Mise à jour ClamAV échouée" + systemctl enable clamav-freshclam clamav-daemon 2>/dev/null || true + systemctl start clamav-freshclam 2>/dev/null && print_success "freshclam démarré" || print_warning "freshclam non démarré" + sleep 3 + systemctl start clamav-daemon 2>/dev/null && print_success "clamav-daemon démarré" || print_warning "clamav-daemon non démarré" + print_success "ClamAV configuré" + mark_step_done "$step_name" +} + +configure_chrony() { + local step_name="configure_chrony" + check_step_done "$step_name" && { skip_step "${STEP_DESCRIPTIONS[$step_name]}"; return 0; } + print_step "Configuration de la synchronisation horaire" + detect_container && { print_warning "Conteneur détecté - NTP géré par l'hôte"; mark_step_done "$step_name"; return 0; } + backup_file "/etc/chrony/chrony.conf" + cat > /etc/chrony/chrony.conf << EOF +pool 2.debian.pool.ntp.org iburst +server 0.fr.pool.ntp.org iburst +server 1.fr.pool.ntp.org iburst +server 2.fr.pool.ntp.org iburst +server 3.fr.pool.ntp.org iburst +driftfile /var/lib/chrony/chrony.drift +logdir /var/log/chrony +makestep 1.0 3 +rtcsync +allow 127.0.0.1 +deny all +local stratum 10 +EOF + timedatectl set-timezone "$AUTO_TIMEZONE" 2>/dev/null || print_warning "Impossible de définir le fuseau horaire $AUTO_TIMEZONE" + systemctl restart chrony 2>/dev/null && print_success "Chrony configuré" || print_warning "Erreur démarrage Chrony" + mark_step_done "$step_name" +} + +harden_ssh() { + local step_name="harden_ssh" + check_step_done "$step_name" && { skip_step "${STEP_DESCRIPTIONS[$step_name]}"; return 0; } + print_step "Durcissement du service SSH" + backup_file "/etc/ssh/sshd_config" + local sshd_config="/etc/ssh/sshd_config" + local ssh_port="22" + detect_lxc || { ssh_port="$AUTO_SSH_PORT"; [[ "$ssh_port" =~ ^[0-9]+$ ]] && [ "$ssh_port" -ge 1 ] && [ "$ssh_port" -le 65535 ] || ssh_port="22"; } + print_info "Configuration SSH avec le port: $ssh_port" + local temp_config + temp_config=$(mktemp) + { + echo "# Configuration SSH sécurisée - $(date)" + if [[ "$ssh_port" != "22" ]] && ! detect_lxc; then + echo "Port 22" + echo "Port $ssh_port" + else + echo "Port $ssh_port" + fi + echo "ListenAddress 0.0.0.0" + echo "ListenAddress ::" + [[ "$AUTO_DISABLE_ROOT_LOGIN" == "yes" ]] && echo "PermitRootLogin no" || echo "PermitRootLogin prohibit-password" + echo "PasswordAuthentication no" + echo "PubkeyAuthentication yes" + echo "PermitEmptyPasswords no" + echo "MaxAuthTries 3" + echo "LoginGraceTime 60" + echo "ClientAliveInterval 300" + echo "ClientAliveCountMax 2" + echo "MaxSessions 2" + echo "Protocol 2" + echo "StrictModes yes" + echo "UseDNS no" + echo "IgnoreRhosts yes" + echo "HostbasedAuthentication no" + echo "PrintLastLog yes" + echo "X11Forwarding no" + echo "AllowAgentForwarding no" + echo "AllowTcpForwarding no" + echo "TCPKeepAlive no" + echo "Compression no" + echo "LogLevel VERBOSE" + echo "Banner /etc/issue.net" + echo "Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr" + echo "MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com" + echo "KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256" + echo "HostKeyAlgorithms ssh-ed25519,ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256,rsa-sha2-256-cert-v01@openssh.com" + } > "$temp_config" + if ! sshd -t -f "$temp_config" 2>&1; then + print_warning "Configuration étendue invalide, passage en mode minimal" + cat > "$temp_config" << 'EOF' +Port 22 +Protocol 2 +PermitRootLogin prohibit-password +PasswordAuthentication no +PubkeyAuthentication yes +PermitEmptyPasswords no +X11Forwarding no +AllowTcpForwarding no +AllowAgentForwarding no +UsePAM yes +PrintLastLog yes +TCPKeepAlive no +ClientAliveInterval 300 +ClientAliveCountMax 2 +EOF + if ! sshd -t -f "$temp_config" 2>&1; then + print_error "Configuration SSH minimale invalide" + rm -f "$temp_config" + return 1 + fi + fi + cp "$temp_config" "$sshd_config" + chmod 600 "$sshd_config" + chown root:root "$sshd_config" + rm -f "$temp_config" + if sshd -t -f "$sshd_config" 2>&1 | tee -a "$LOG_FILE"; then + systemctl restart sshd 2>/dev/null || systemctl restart ssh 2>/dev/null && \ + print_success "SSH durci avec succès (port $ssh_port)" || { + print_error "Échec redémarrage SSH - restauration" + cp "${BACKUP_DIR}/sshd_config" /etc/ssh/sshd_config + systemctl restart sshd 2>/dev/null || systemctl restart ssh 2>/dev/null + return 1 + } + else + print_error "Configuration SSH invalide" + cp "${BACKUP_DIR}/sshd_config" /etc/ssh/sshd_config + return 1 + fi + mark_step_done "$step_name" +} + +configure_firewall_ports() { + local step_name="configure_firewall_ports" + check_step_done "$step_name" && { skip_step "${STEP_DESCRIPTIONS[$step_name]}"; return 0; } + [[ "$AUTO_ENABLE_UFW" != "yes" ]] && { mark_step_done "$step_name"; return 0; } + print_step "Configuration des règles de pare-feu" + if detect_container; then + print_warning "Conteneur détecté - pare-feu géré par l'hôte" + command -v ufw > /dev/null 2>&1 && { ufw --force disable 2>/dev/null || true; systemctl stop ufw 2>/dev/null || true; systemctl disable ufw 2>/dev/null || true; } + mark_step_done "$step_name"; return 0 + fi + ufw --force disable > /dev/null 2>&1 || true + ufw --force reset > /dev/null 2>&1 + ufw default deny incoming + ufw default allow outgoing + local ssh_port + ssh_port=$(get_ssh_port_to_use) + ufw allow "${ssh_port}/tcp" comment 'SSH sécurisé' + [[ "$ssh_port" != "22" ]] && is_port_open "22" && ufw allow 22/tcp comment 'SSH temporaire' + ufw allow 22/tcp comment 'SSH' + ufw allow 53/udp comment 'DNS' + ufw allow 123/udp comment 'NTP' + if [[ -f "$OPEN_PORTS_FILE" ]]; then + while read -r port; do + [[ "$port" == "22" || "$port" == "$ssh_port" || "$port" == "53" || "$port" == "123" ]] && continue + local service_name="" + case $port in + 80) service_name="HTTP" ;; 443) service_name="HTTPS" ;; 25) service_name="SMTP" ;; + 587) service_name="SMTP Submission" ;; 465) service_name="SMTPS" ;; + 993) service_name="IMAPS" ;; 995) service_name="POP3S" ;; + 3306) service_name="MySQL" ;; 5432) service_name="PostgreSQL" ;; + 6379) service_name="Redis" ;; 27017) service_name="MongoDB" ;; + 3000) service_name="Grafana/WebApp" ;; 8080) service_name="Proxy/Web" ;; 8443) service_name="HTTPS Alt" ;; + *) ufw deny "${port}/tcp" comment "Port non standard $port (bloqué auto)"; continue ;; + esac + [[ -n "$service_name" ]] && ufw allow "${port}/tcp" comment "$service_name" + done < "$OPEN_PORTS_FILE" + fi + echo "y" | ufw --force enable > /dev/null 2>&1 + ufw status | grep -q "Status: active" && print_success "Pare-feu UFW configuré et activé" || print_warning "UFW activé avec avertissements" + mark_step_done "$step_name" +} + +configure_fail2ban() { + local step_name="configure_fail2ban" + check_step_done "$step_name" && { skip_step "${STEP_DESCRIPTIONS[$step_name]}"; return 0; } + [[ "$AUTO_ENABLE_FAIL2BAN" != "yes" ]] && { mark_step_done "$step_name"; return 0; } + print_step "Configuration de Fail2ban" + backup_file "/etc/fail2ban/jail.conf" + local ssh_port + ssh_port=$(get_ssh_port_to_use) + cat > /etc/fail2ban/jail.local << EOF +[DEFAULT] +bantime = 1h +findtime = 10m +maxretry = 3 +ignoreip = 127.0.0.1/8 ::1 +backend = auto +banaction = ufw +action = %(action_mwl)s + +[sshd] +enabled = true +port = $ssh_port +filter = sshd +logpath = /var/log/auth.log +maxretry = 5 +bantime = 2h + +[recidive] +enabled = true +filter = recidive +logpath = /var/log/fail2ban.log +action = iptables-allports[name=recidive] +bantime = 1w +findtime = 1d +maxretry = 3 +EOF + systemctl enable fail2ban 2>/dev/null || true + systemctl restart fail2ban 2>&1 | tee -a "$LOG_FILE" && print_success "Fail2ban configuré" || print_warning "Fail2ban démarré avec avertissements" + mark_step_done "$step_name" +} + +remove_unneeded_packages() { + local step_name="remove_unneeded_packages" + check_step_done "$step_name" && { skip_step "${STEP_DESCRIPTIONS[$step_name]}"; return 0; } + print_step "Suppression des paquets inutiles" + for package in telnet rsh-client rsh-server netcat-openbsd netcat-traditional nis talk talkd; do + dpkg -l | grep -q "^ii.*${package}" && { DEBIAN_FRONTEND=noninteractive apt-get purge -y -qq "$package" 2>/dev/null; print_info "Paquet $package supprimé"; } + done + DEBIAN_FRONTEND=noninteractive apt-get autoremove -y -qq + print_success "Paquets inutiles supprimés" + mark_step_done "$step_name" +} + +restrict_file_permissions() { + local step_name="restrict_file_permissions" + check_step_done "$step_name" && { skip_step "${STEP_DESCRIPTIONS[$step_name]}"; return 0; } + print_step "Restriction des permissions des fichiers critiques" + chmod 644 /etc/passwd 2>/dev/null || true + chmod 600 /etc/shadow 2>/dev/null || true + chmod 644 /etc/group 2>/dev/null || true + chmod 600 /etc/gshadow 2>/dev/null || true + chmod 600 /etc/sudoers 2>/dev/null || true + chmod 750 /etc/sudoers.d 2>/dev/null || true + chmod 600 /boot/grub/grub.cfg 2>/dev/null || true + chmod 644 /etc/ssh/ssh_config 2>/dev/null || true + chmod 600 /etc/ssh/sshd_config 2>/dev/null || true + print_success "Permissions des fichiers critiques restreintes" + mark_step_done "$step_name" +} + +disable_risky_kernel_modules() { + local step_name="disable_risky_kernel_modules" + check_step_done "$step_name" && { skip_step "${STEP_DESCRIPTIONS[$step_name]}"; return 0; } + print_step "Désactivation des modules noyau risqués" + cat > /etc/modprobe.d/hardening-blacklist.conf << 'EOF' +blacklist dccp +install dccp /bin/true +blacklist sctp +install sctp /bin/true +blacklist rds +install rds /bin/true +blacklist tipc +install tipc /bin/true +blacklist cramfs +install cramfs /bin/true +blacklist freevxfs +install freevxfs /bin/true +blacklist jffs2 +install jffs2 /bin/true +blacklist hfs +install hfs /bin/true +blacklist hfsplus +install hfsplus /bin/true +blacklist squashfs +install squashfs /bin/true +blacklist udf +install udf /bin/true +blacklist firewire-core +install firewire-core /bin/true +blacklist thunderbolt +install thunderbolt /bin/true +EOF + print_success "Modules noyau risqués désactivés" + mark_step_done "$step_name" +} + +configure_security_limits() { + local step_name="configure_security_limits" + check_step_done "$step_name" && { skip_step "${STEP_DESCRIPTIONS[$step_name]}"; return 0; } + print_step "Configuration des limites de sécurité" + backup_file "/etc/security/limits.conf" + cat >> /etc/security/limits.conf << 'EOF' +* hard core 0 +* soft nproc 512 +* hard nproc 1024 +* soft nofile 65536 +* hard nofile 65536 +EOF + print_success "Limites de sécurité configurées" + mark_step_done "$step_name" +} + +verify_packages_integrity() { + local step_name="verify_packages_integrity" + check_step_done "$step_name" && { skip_step "${STEP_DESCRIPTIONS[$step_name]}"; return 0; } + print_step "Vérification de l'intégrité des paquets" + command -v debsums > /dev/null 2>&1 || { DEBIAN_FRONTEND=noninteractive apt-get install -y -qq debsums 2>/dev/null || { print_warning "Impossible d'installer debsums"; mark_step_done "$step_name"; return 0; }; } + local debsums_output + debsums_output=$(debsums -s 2>&1 | grep -v "Permission denied" || true) + if [[ -n "$debsums_output" ]]; then + echo "$debsums_output" | tee -a "$LOG_FILE" + print_warning "Certains paquets ont des erreurs d'intégrité" + else + print_success "Tous les paquets sont intacts" + fi + mark_step_done "$step_name" +} + +configure_aide_cron() { + local step_name="configure_aide_cron" + check_step_done "$step_name" && { skip_step "${STEP_DESCRIPTIONS[$step_name]}"; return 0; } + [[ "$AUTO_ENABLE_AIDE" != "yes" ]] && { mark_step_done "$step_name"; return 0; } + print_step "Configuration des vérifications AIDE planifiées" + cat > /etc/cron.daily/aide-check << 'EOF' +#!/bin/bash +LOGFILE="/var/log/aide-check-$(date +%Y%m%d).log" +command -v aide > /dev/null || exit 0 +echo "=== Vérification AIDE $(date) ===" > "$LOGFILE" +/usr/bin/aide --check 2>&1 >> "$LOGFILE" && \ + echo "AIDE: OK - $(date)" >> "$LOGFILE" || \ + { echo "AIDE: ALERTE - $(date)" >> "$LOGFILE"; echo "Alerte AIDE sur $(hostname)" | mail -s "[AIDE] Changements détectés" root 2>/dev/null || true; } +find /var/log -name "aide-check-*.log" -mtime +30 -delete 2>/dev/null || true +EOF + chmod 755 /etc/cron.daily/aide-check + print_success "Vérification AIDE quotidienne configurée" + mark_step_done "$step_name" +} + +harden_smtp_banner() { + local step_name="harden_smtp_banner" + check_step_done "$step_name" && { skip_step "${STEP_DESCRIPTIONS[$step_name]}"; return 0; } + print_step "Durcissement bannière SMTP" + if dpkg -l | grep -q "^ii.*postfix"; then + backup_file "/etc/postfix/main.cf" + postconf -e "smtpd_banner = \$myhostname ESMTP" + postconf -e "disable_vrfy_command = yes" + systemctl reload postfix 2>/dev/null || true + print_success "Bannière Postfix durcie" + else + print_info "Aucun serveur SMTP détecté" + fi + mark_step_done "$step_name" +} + +harden_systemd_services() { + local step_name="harden_systemd_services" + check_step_done "$step_name" && { skip_step "${STEP_DESCRIPTIONS[$step_name]}"; return 0; } + print_step "Durcissement services systemd" + command -v systemd-analyze > /dev/null || { print_warning "systemd-analyze non disponible"; mark_step_done "$step_name"; return 0; } + if detect_container; then + for service in ssh sshd fail2ban chrony; do + rm -rf "/etc/systemd/system/${service}.service.d" 2>/dev/null || true + done + systemctl daemon-reload + print_success "Configuration systemd nettoyée (conteneur)" + mark_step_done "$step_name"; return 0 + fi + local hardened=0 + for service in ssh sshd fail2ban chrony; do + local unit="${service}.service" + systemctl list-unit-files | grep -q "^${unit}" || continue + mkdir -p "/etc/systemd/system/${unit}.d" + cat > "/etc/systemd/system/${unit}.d/security.conf" << 'EOF' +[Service] +NoNewPrivileges=yes +PrivateTmp=yes +ProtectSystem=full +ProtectHome=read-only +ProtectKernelTunables=yes +ProtectKernelModules=yes +ProtectControlGroups=yes +RestrictRealtime=yes +EOF + hardened=$((hardened + 1)) + done + systemctl daemon-reload + print_success "$hardened service(s) systemd durci(s)" + mark_step_done "$step_name" +} + +configure_advanced_pam() { + local step_name="configure_advanced_pam" + check_step_done "$step_name" && { skip_step "${STEP_DESCRIPTIONS[$step_name]}"; return 0; } + print_step "Configuration PAM avancée" + backup_file "/etc/pam.d/common-password" + grep -q "pam_unix.so" /etc/pam.d/common-password && { + grep -q "sha512" /etc/pam.d/common-password || sed -i 's/pam_unix.so.*/& sha512/' /etc/pam.d/common-password + grep -q "rounds=" /etc/pam.d/common-password || sed -i 's/pam_unix.so.*/& rounds=500000/' /etc/pam.d/common-password + } + print_info "Application expiration mots de passe aux utilisateurs..." + while IFS=: read -r user _ uid _ _ _ shell; do + [[ "$uid" -ge 1000 || "$user" == "root" ]] && [[ -n "$shell" ]] && [[ "$shell" != *"nologin"* ]] && [[ "$shell" != *"false"* ]] && { + chage -M 90 "$user" 2>/dev/null || true + } + done < /etc/passwd + print_success "Configuration PAM avancée appliquée" + mark_step_done "$step_name" +} + +check_partition_layout() { + local step_name="check_partition_layout" + check_step_done "$step_name" && { skip_step "${STEP_DESCRIPTIONS[$step_name]}"; return 0; } + print_step "Vérification disposition partitions" + local warnings=0 + for partition in /home /tmp /var /var/log /var/log/audit; do + mount | grep -q " on ${partition} " && print_info " ✓ $partition: Partition séparée" || { warnings=$((warnings+1)); print_warning " ⚠ $partition: Non monté séparément"; } + done + [[ $warnings -eq 0 ]] && print_success "Toutes les partitions critiques séparées" || print_warning "$warnings partition(s) non séparée(s)" + mark_step_done "$step_name" +} + +check_vmlinuz() { + local step_name="check_vmlinuz" + check_step_done "$step_name" && { skip_step "${STEP_DESCRIPTIONS[$step_name]}"; return 0; } + print_step "Vérification fichiers noyau" + local found=0 + for kfile in /vmlinuz /boot/vmlinuz "/boot/vmlinuz-$(uname -r)" "/boot/initrd.img-$(uname -r)"; do + [[ -f "$kfile" ]] && { found=$((found+1)); print_info " ✓ $kfile"; } + done + [[ $found -eq 0 ]] && print_warning "Aucun fichier noyau trouvé" || print_success "$found fichier(s) noyau trouvé(s)" + mark_step_done "$step_name" +} + +run_chkrootkit() { + local step_name="run_chkrootkit" + check_step_done "$step_name" && { skip_step "${STEP_DESCRIPTIONS[$step_name]}"; return 0; } + print_step "Exécution de chkrootkit" + mkdir -p "$BACKUP_DIR" + chkrootkit > "$BACKUP_DIR/chkrootkit_report.log" 2>&1 || print_warning "Chkrootkit a détecté des avertissements" + print_success "Scan chkrootkit terminé - rapport: $BACKUP_DIR/chkrootkit_report.log" + mark_step_done "$step_name" +} + +prepare_ssh_cleanup() { + local step_name="prepare_ssh_cleanup" + check_step_done "$step_name" && { skip_step "${STEP_DESCRIPTIONS[$step_name]}"; return 0; } + print_step "Préparation nettoyage port SSH 22" + local ssh_port + ssh_port=$(get_ssh_port_to_use) + [[ "$ssh_port" != "22" ]] && print_info "Après test du port $ssh_port: relancez avec --cleanup-ssh" || print_info "Port SSH principal: 22 - aucun nettoyage nécessaire" + mark_step_done "$step_name" +} + +run_lynis_audit() { + local step_name="run_lynis_audit" + check_step_done "$step_name" && { skip_step "${STEP_DESCRIPTIONS[$step_name]}"; return 0; } + [[ "$AUTO_SKIP_LYNIS" == "yes" ]] && { mark_step_done "$step_name"; return 0; } + print_step "Exécution de l'audit Lynis" + lynis audit system --quick --no-colors > "$SECURITY_REPORT" 2>&1 + local score + score=$(grep -i "Hardening index" "$SECURITY_REPORT" | grep -oP '\d+' | head -1 || echo "0") + print_success "Audit Lynis terminé - Score: ${score}/100 - Rapport: $SECURITY_REPORT" + mark_step_done "$step_name" +} + +configure_automatic_updates() { + local step_name="configure_automatic_updates" + check_step_done "$step_name" && { skip_step "${STEP_DESCRIPTIONS[$step_name]}"; return 0; } + print_step "Configuration des mises à jour automatiques" + dpkg -l | grep -q "^ii.*unattended-upgrades" || DEBIAN_FRONTEND=noninteractive apt-get install -y -qq unattended-upgrades apt-listchanges needrestart 2>/dev/null || true + backup_file "/etc/apt/apt.conf.d/50unattended-upgrades" + cat > /etc/apt/apt.conf.d/50unattended-upgrades << 'EOF' +Unattended-Upgrade::Allowed-Origins { + "${distro_id}:${distro_codename}"; + "${distro_id}:${distro_codename}-security"; + "${distro_id}:${distro_codename}-updates"; +}; +Unattended-Upgrade::AutoFixInterruptedDpkg "true"; +Unattended-Upgrade::MinimalSteps "true"; +Unattended-Upgrade::Remove-Unused-Kernel-Packages "true"; +Unattended-Upgrade::Remove-Unused-Dependencies "true"; +Unattended-Upgrade::Automatic-Reboot "false"; +Unattended-Upgrade::Mail "root"; +Unattended-Upgrade::MailOnlyOnError "true"; +EOF + cat > /etc/apt/apt.conf.d/10periodic << 'EOF' +APT::Periodic::Update-Package-Lists "1"; +APT::Periodic::Download-Upgradeable-Packages "1"; +APT::Periodic::AutocleanInterval "7"; +APT::Periodic::Unattended-Upgrade "1"; +APT::Periodic::Verbose "0"; +EOF + systemctl enable unattended-upgrades 2>/dev/null || true + systemctl restart unattended-upgrades 2>/dev/null || systemctl start unattended-upgrades 2>/dev/null || true + print_success "Mises à jour automatiques configurées" + mark_step_done "$step_name" +} + +cleanup_ssh_port() { + local ssh_port + ssh_port=$(get_ssh_port_to_use) + [[ "$ssh_port" == "22" ]] && { print_info "Port SSH principal déjà sur 22"; return 0; } + backup_file "/etc/ssh/sshd_config" + sed -i '/^Port 22$/d' /etc/ssh/sshd_config + command -v ufw > /dev/null 2>&1 && ufw status | grep -q "22/tcp" && ufw delete allow 22/tcp 2>/dev/null || true + sshd -t && { systemctl restart sshd 2>/dev/null; print_success "Port SSH 22 supprimé - SSH sur port $ssh_port uniquement"; } || { + print_error "Erreur configuration - restauration" + cp "${BACKUP_DIR}/sshd_config" /etc/ssh/sshd_config + systemctl restart sshd 2>/dev/null + return 1 + } +} + +check_requirements() { + [[ $EUID -ne 0 ]] && { print_error "Ce script doit être exécuté en tant que root"; exit 1; } + [[ ! -f /etc/debian_version ]] && print_warning "Système non Debian/Ubuntu - compatibilité limitée" + mkdir -p "$BACKUP_DIR" + touch "$LOG_FILE" "$STATUS_FILE" 2>/dev/null || { print_error "Impossible de créer les fichiers de log"; exit 1; } + print_success "Prérequis vérifiés" +} + +parse_arguments() { + while [[ $# -gt 0 ]]; do + case $1 in + --force-all) FORCE_ALL=true; shift ;; + --force-step=*) FORCE_STEPS+=("${1#*=}"); shift ;; + --skip-step=*) SKIP_STEPS+=("${1#*=}"); shift ;; + --cleanup-ssh) AUTO_CLEANUP_SSH="yes"; shift ;; + --unattended) UNATTENDED=true; AUTO_YES="yes"; shift ;; + --ssh-port=*) AUTO_SSH_PORT="${1#*=}"; shift ;; + --timezone=*) AUTO_TIMEZONE="${1#*=}"; shift ;; + --change-root-pwd) AUTO_CHANGE_ROOT_PWD="yes"; shift ;; + --yes|-y) AUTO_YES="yes"; shift ;; + --skip-lynis) AUTO_SKIP_LYNIS="yes"; shift ;; + *) print_error "Option inconnue: $1"; exit 1 ;; + esac + done +} + +main() { + parse_arguments "$@" + check_requirements + + log_message "==================================================" "START" + log_message "Démarrage durcissement système v8.2" "START" + log_message "Mode: $([ "$UNATTENDED" == true ] && echo "Autonome" || echo "Interactif")" "START" + log_message "Port SSH: $AUTO_SSH_PORT" "START" + log_message "Hostname: $(hostname)" "START" + log_message "==================================================" "START" + + install_security_tools || print_warning "install_security_tools: erreur ignorée" + change_root_password || print_warning "change_root_password: erreur ignorée" + detect_open_ports || print_warning "detect_open_ports: erreur ignorée" + configure_process_accounting || print_warning "configure_process_accounting: erreur ignorée" + configure_sysctl_security || print_warning "configure_sysctl_security: erreur ignorée" + configure_log_permissions || print_warning "configure_log_permissions: erreur ignorée" + configure_pam_password_policy || print_warning "configure_pam_password_policy: erreur ignorée" + configure_login_defs || print_warning "configure_login_defs: erreur ignorée" + configure_umask || print_warning "configure_umask: erreur ignorée" + configure_aide_sha512 || print_warning "configure_aide_sha512: erreur ignorée" + initialize_aide_db || print_warning "initialize_aide_db: erreur ignorée" + [[ "$AUTO_ENABLE_CLAMAV" == "yes" ]] && { configure_clamav || print_warning "configure_clamav: erreur ignorée"; } + configure_chrony || print_warning "configure_chrony: erreur ignorée" + harden_ssh || print_warning "harden_ssh: erreur ignorée" + configure_banners || print_warning "configure_banners: erreur ignorée" + configure_firewall_ports || print_warning "configure_firewall_ports: erreur ignorée" + configure_fail2ban || print_warning "configure_fail2ban: erreur ignorée" + remove_unneeded_packages || print_warning "remove_unneeded_packages: erreur ignorée" + restrict_file_permissions || print_warning "restrict_file_permissions: erreur ignorée" + disable_risky_kernel_modules || print_warning "disable_risky_kernel_modules: erreur ignorée" + configure_security_limits || print_warning "configure_security_limits: erreur ignorée" + verify_packages_integrity || print_warning "verify_packages_integrity: erreur ignorée" + configure_automatic_updates || print_warning "configure_automatic_updates: erreur ignorée" + configure_aide_cron || print_warning "configure_aide_cron: erreur ignorée" + harden_smtp_banner || print_warning "harden_smtp_banner: erreur ignorée" + harden_systemd_services || print_warning "harden_systemd_services: erreur ignorée" + configure_advanced_pam || print_warning "configure_advanced_pam: erreur ignorée" + check_partition_layout || print_warning "check_partition_layout: erreur ignorée" + check_vmlinuz || print_warning "check_vmlinuz: erreur ignorée" + run_chkrootkit || print_warning "run_chkrootkit: erreur ignorée" + prepare_ssh_cleanup || print_warning "prepare_ssh_cleanup: erreur ignorée" + run_lynis_audit || print_warning "run_lynis_audit: erreur ignorée" + + [[ "$AUTO_CLEANUP_SSH" == "yes" ]] && { cleanup_ssh_port || print_warning "cleanup_ssh_port: erreur ignorée"; } + + log_message "Durcissement terminé" "END" + log_message "==================================================" "END" +} + +trap 'print_error "Script interrompu"; exit 130' INT TERM +[[ "${BASH_SOURCE[0]}" == "${0}" ]] && main "$@" diff --git a/securecheck/catalog.py b/securecheck/catalog.py index 5b1254b..d64e37d 100644 --- a/securecheck/catalog.py +++ b/securecheck/catalog.py @@ -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] diff --git a/securecheck/logging_utils.py b/securecheck/logging_utils.py index f08d685..28e8ca7 100644 --- a/securecheck/logging_utils.py +++ b/securecheck/logging_utils.py @@ -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", diff --git a/securecheck/status.py b/securecheck/status.py index 5a2c40f..dcfe556 100644 --- a/securecheck/status.py +++ b/securecheck/status.py @@ -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"), diff --git a/securecheck/summary_utils.py b/securecheck/summary_utils.py new file mode 100644 index 0000000..a211da7 --- /dev/null +++ b/securecheck/summary_utils.py @@ -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 diff --git a/securecheck/tasks.py b/securecheck/tasks.py index f281f6f..e580fc6 100644 --- a/securecheck/tasks.py +++ b/securecheck/tasks.py @@ -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,