Initial commit

This commit is contained in:
Johnny
2026-04-05 18:56:26 +02:00
parent 9f5e146229
commit 751dc8892c
43 changed files with 4278 additions and 0 deletions

0
.codex Normal file
View File

View File

@@ -0,0 +1,46 @@
2026-04-05 16:13:09 | INFO | [1/5] Mise à jour système
2026-04-05 16:13:09 | INFO | Commande: sudo apt-get update
2026-04-05 16:13:09 | INFO | [dry-run] sudo apt-get update
2026-04-05 16:13:09 | INFO | Commande: sudo apt-get dist-upgrade -y
2026-04-05 16:13:09 | INFO | [dry-run] sudo apt-get dist-upgrade -y
2026-04-05 16:13:09 | INFO | Commande: sudo apt-get autoremove -y
2026-04-05 16:13:09 | INFO | [dry-run] sudo apt-get autoremove -y
2026-04-05 16:13:09 | INFO | Commande: sudo apt-get autoclean
2026-04-05 16:13:09 | INFO | [dry-run] sudo apt-get autoclean
2026-04-05 16:13:09 | INFO | -> OK (0.0s)
2026-04-05 16:13:09 | INFO | [2/5] Audit Lynis
2026-04-05 16:13:09 | INFO | Commande: sudo lynis audit system --quick
2026-04-05 16:13:09 | INFO | [dry-run] sudo lynis audit system --quick
2026-04-05 16:13:09 | INFO | Ecriture du fichier /home/tuxgyver/scripts/securecheck/.securecheck-runtime/state/logs/reports/20260405-161309-lynis.log
2026-04-05 16:13:09 | INFO | [dry-run] write /home/tuxgyver/scripts/securecheck/.securecheck-runtime/state/logs/reports/20260405-161309-lynis.log
2026-04-05 16:13:09 | INFO | -> OK (0.0s)
2026-04-05 16:13:09 | INFO | [3/5] Vérification rootkits
2026-04-05 16:13:09 | INFO | Commande: sudo apt-get install -y chkrootkit
2026-04-05 16:13:09 | INFO | [dry-run] sudo apt-get install -y chkrootkit
2026-04-05 16:13:09 | INFO | Commande: sudo rkhunter --update
2026-04-05 16:13:09 | INFO | [dry-run] sudo rkhunter --update
2026-04-05 16:13:09 | INFO | Commande: sudo rkhunter --propupd
2026-04-05 16:13:09 | INFO | [dry-run] sudo rkhunter --propupd
2026-04-05 16:13:09 | INFO | Commande: sudo rkhunter --check --skip-keypress --report-warnings-only
2026-04-05 16:13:09 | INFO | [dry-run] sudo rkhunter --check --skip-keypress --report-warnings-only
2026-04-05 16:13:09 | INFO | Commande: sudo chkrootkit -q
2026-04-05 16:13:09 | INFO | [dry-run] sudo chkrootkit -q
2026-04-05 16:13:09 | INFO | Ecriture du fichier /home/tuxgyver/scripts/securecheck/.securecheck-runtime/state/logs/reports/20260405-161309-rootkit-report.json
2026-04-05 16:13:09 | INFO | [dry-run] write /home/tuxgyver/scripts/securecheck/.securecheck-runtime/state/logs/reports/20260405-161309-rootkit-report.json
2026-04-05 16:13:09 | INFO | -> OK (0.0s)
2026-04-05 16:13:09 | INFO | [4/5] Vérification / autoconfig du firewall
2026-04-05 16:13:09 | INFO | Commande: sudo ufw default deny incoming
2026-04-05 16:13:09 | INFO | [dry-run] sudo ufw default deny incoming
2026-04-05 16:13:09 | INFO | Commande: sudo ufw default allow outgoing
2026-04-05 16:13:09 | INFO | [dry-run] sudo ufw default allow outgoing
2026-04-05 16:13:09 | INFO | Commande: sudo ufw status
2026-04-05 16:13:09 | INFO | [dry-run] sudo ufw status
2026-04-05 16:13:09 | INFO | Commande: sudo ufw allow 22/tcp
2026-04-05 16:13:09 | INFO | [dry-run] sudo ufw allow 22/tcp
2026-04-05 16:13:09 | INFO | Commande: sudo ufw --force enable
2026-04-05 16:13:09 | INFO | [dry-run] sudo ufw --force enable
2026-04-05 16:13:09 | INFO | -> OK (0.0s)
2026-04-05 16:13:09 | INFO | [5/5] Rotation des logs
2026-04-05 16:13:09 | INFO | Ecriture du fichier /etc/logrotate.d/securecheck
2026-04-05 16:13:09 | INFO | [dry-run] write /etc/logrotate.d/securecheck
2026-04-05 16:13:09 | INFO | -> OK (0.0s)

View File

@@ -0,0 +1,46 @@
2026-04-05 16:13:28 | INFO | [1/5] Mise à jour système
2026-04-05 16:13:28 | INFO | Commande: sudo apt-get update
2026-04-05 16:13:28 | INFO | [dry-run] sudo apt-get update
2026-04-05 16:13:28 | INFO | Commande: sudo apt-get dist-upgrade -y
2026-04-05 16:13:28 | INFO | [dry-run] sudo apt-get dist-upgrade -y
2026-04-05 16:13:28 | INFO | Commande: sudo apt-get autoremove -y
2026-04-05 16:13:28 | INFO | [dry-run] sudo apt-get autoremove -y
2026-04-05 16:13:28 | INFO | Commande: sudo apt-get autoclean
2026-04-05 16:13:28 | INFO | [dry-run] sudo apt-get autoclean
2026-04-05 16:13:28 | INFO | -> OK (0.0s)
2026-04-05 16:13:28 | INFO | [2/5] Audit Lynis
2026-04-05 16:13:28 | INFO | Commande: sudo lynis audit system --quick
2026-04-05 16:13:28 | INFO | [dry-run] sudo lynis audit system --quick
2026-04-05 16:13:28 | INFO | Ecriture du fichier /home/tuxgyver/scripts/securecheck/.securecheck-runtime/state/logs/reports/20260405-161328-lynis.log
2026-04-05 16:13:28 | INFO | [dry-run] write /home/tuxgyver/scripts/securecheck/.securecheck-runtime/state/logs/reports/20260405-161328-lynis.log
2026-04-05 16:13:28 | INFO | -> OK (0.0s)
2026-04-05 16:13:28 | INFO | [3/5] Vérification rootkits
2026-04-05 16:13:28 | INFO | Commande: sudo apt-get install -y chkrootkit
2026-04-05 16:13:28 | INFO | [dry-run] sudo apt-get install -y chkrootkit
2026-04-05 16:13:28 | INFO | Commande: sudo rkhunter --update
2026-04-05 16:13:28 | INFO | [dry-run] sudo rkhunter --update
2026-04-05 16:13:28 | INFO | Commande: sudo rkhunter --propupd
2026-04-05 16:13:28 | INFO | [dry-run] sudo rkhunter --propupd
2026-04-05 16:13:28 | INFO | Commande: sudo rkhunter --check --skip-keypress --report-warnings-only
2026-04-05 16:13:28 | INFO | [dry-run] sudo rkhunter --check --skip-keypress --report-warnings-only
2026-04-05 16:13:28 | INFO | Commande: sudo chkrootkit -q
2026-04-05 16:13:28 | INFO | [dry-run] sudo chkrootkit -q
2026-04-05 16:13:28 | INFO | Ecriture du fichier /home/tuxgyver/scripts/securecheck/.securecheck-runtime/state/logs/reports/20260405-161328-rootkit-report.json
2026-04-05 16:13:28 | INFO | [dry-run] write /home/tuxgyver/scripts/securecheck/.securecheck-runtime/state/logs/reports/20260405-161328-rootkit-report.json
2026-04-05 16:13:28 | INFO | -> OK (0.0s)
2026-04-05 16:13:28 | INFO | [4/5] Vérification / autoconfig du firewall
2026-04-05 16:13:28 | INFO | Commande: sudo ufw default deny incoming
2026-04-05 16:13:28 | INFO | [dry-run] sudo ufw default deny incoming
2026-04-05 16:13:28 | INFO | Commande: sudo ufw default allow outgoing
2026-04-05 16:13:28 | INFO | [dry-run] sudo ufw default allow outgoing
2026-04-05 16:13:28 | INFO | Commande: sudo ufw status
2026-04-05 16:13:28 | INFO | [dry-run] sudo ufw status
2026-04-05 16:13:28 | INFO | Commande: sudo ufw allow 22/tcp
2026-04-05 16:13:28 | INFO | [dry-run] sudo ufw allow 22/tcp
2026-04-05 16:13:28 | INFO | Commande: sudo ufw --force enable
2026-04-05 16:13:28 | INFO | [dry-run] sudo ufw --force enable
2026-04-05 16:13:28 | INFO | -> OK (0.0s)
2026-04-05 16:13:28 | INFO | [5/5] Rotation des logs
2026-04-05 16:13:28 | INFO | Ecriture du fichier /etc/logrotate.d/securecheck
2026-04-05 16:13:28 | INFO | [dry-run] write /etc/logrotate.d/securecheck
2026-04-05 16:13:28 | INFO | -> OK (0.0s)

View File

@@ -0,0 +1,44 @@
2026-04-05 18:24:45 | INFO | [1/5] Mise à jour système
2026-04-05 18:24:45 | INFO | Commande: sudo apt-get update
2026-04-05 18:24:45 | INFO | [dry-run] sudo apt-get update
2026-04-05 18:24:45 | INFO | Commande: sudo apt-get dist-upgrade -y
2026-04-05 18:24:45 | INFO | [dry-run] sudo apt-get dist-upgrade -y
2026-04-05 18:24:45 | INFO | Commande: sudo apt-get autoremove -y
2026-04-05 18:24:45 | INFO | [dry-run] sudo apt-get autoremove -y
2026-04-05 18:24:45 | INFO | Commande: sudo apt-get autoclean
2026-04-05 18:24:45 | INFO | [dry-run] sudo apt-get autoclean
2026-04-05 18:24:45 | INFO | -> OK (0.0s)
2026-04-05 18:24:45 | INFO | [2/5] Audit Lynis
2026-04-05 18:24:45 | INFO | Commande: sudo lynis audit system --quick
2026-04-05 18:24:45 | INFO | [dry-run] sudo lynis audit system --quick
2026-04-05 18:24:45 | INFO | Ecriture du fichier /home/tuxgyver/scripts/securecheck/.securecheck-runtime/state/logs/reports/20260405-182445-lynis.log
2026-04-05 18:24:45 | INFO | [dry-run] write /home/tuxgyver/scripts/securecheck/.securecheck-runtime/state/logs/reports/20260405-182445-lynis.log
2026-04-05 18:24:45 | INFO | -> OK (0.0s)
2026-04-05 18:24:45 | INFO | [3/5] Vérification rootkits
2026-04-05 18:24:45 | INFO | Commande: sudo rkhunter --update
2026-04-05 18:24:45 | INFO | [dry-run] sudo rkhunter --update
2026-04-05 18:24:45 | INFO | Commande: sudo rkhunter --propupd
2026-04-05 18:24:45 | INFO | [dry-run] sudo rkhunter --propupd
2026-04-05 18:24:45 | INFO | Commande: sudo rkhunter --check --skip-keypress --report-warnings-only
2026-04-05 18:24:45 | INFO | [dry-run] sudo rkhunter --check --skip-keypress --report-warnings-only
2026-04-05 18:24:45 | INFO | Commande: sudo chkrootkit -q
2026-04-05 18:24:45 | INFO | [dry-run] sudo chkrootkit -q
2026-04-05 18:24:45 | INFO | Ecriture du fichier /home/tuxgyver/scripts/securecheck/.securecheck-runtime/state/logs/reports/20260405-182445-rootkit-report.json
2026-04-05 18:24:45 | INFO | [dry-run] write /home/tuxgyver/scripts/securecheck/.securecheck-runtime/state/logs/reports/20260405-182445-rootkit-report.json
2026-04-05 18:24:45 | INFO | -> OK (0.0s)
2026-04-05 18:24:45 | INFO | [4/5] Vérification / autoconfig du firewall
2026-04-05 18:24:45 | INFO | Commande: sudo ufw default deny incoming
2026-04-05 18:24:45 | INFO | [dry-run] sudo ufw default deny incoming
2026-04-05 18:24:45 | INFO | Commande: sudo ufw default allow outgoing
2026-04-05 18:24:45 | INFO | [dry-run] sudo ufw default allow outgoing
2026-04-05 18:24:45 | INFO | Commande: sudo ufw status
2026-04-05 18:24:45 | INFO | [dry-run] sudo ufw status
2026-04-05 18:24:45 | INFO | Commande: sudo ufw allow 22/tcp
2026-04-05 18:24:45 | INFO | [dry-run] sudo ufw allow 22/tcp
2026-04-05 18:24:45 | INFO | Commande: sudo ufw --force enable
2026-04-05 18:24:45 | INFO | [dry-run] sudo ufw --force enable
2026-04-05 18:24:45 | INFO | -> OK (0.0s)
2026-04-05 18:24:45 | INFO | [5/5] Rotation des logs
2026-04-05 18:24:45 | INFO | Ecriture du fichier /etc/logrotate.d/securecheck
2026-04-05 18:24:45 | INFO | [dry-run] write /etc/logrotate.d/securecheck
2026-04-05 18:24:45 | INFO | -> OK (0.0s)

View File

@@ -0,0 +1,18 @@
2026-04-05 18:24:55 | INFO | [1/1] Installation et configuration zsh
2026-04-05 18:24:55 | INFO | Commande: sudo apt-get update
2026-04-05 18:24:55 | INFO | [dry-run] sudo apt-get update
2026-04-05 18:24:55 | INFO | [1/1] Utilitaires pratiques
2026-04-05 18:24:55 | INFO | Commande: sudo apt-get update
2026-04-05 18:24:55 | INFO | [dry-run] sudo apt-get update
2026-04-05 18:24:55 | INFO | Commande: sudo apt-get install -y fonts-powerline
2026-04-05 18:24:55 | INFO | [dry-run] sudo apt-get install -y fonts-powerline
2026-04-05 18:24:55 | INFO | Téléchargement: https://git.h3campus.fr/Johnny/Install_zsh/raw/branch/main/.p10k.zsh
2026-04-05 18:24:55 | INFO | [dry-run] download https://git.h3campus.fr/Johnny/Install_zsh/raw/branch/main/.p10k.zsh
2026-04-05 18:24:55 | INFO | Ecriture du fichier /home/tuxgyver/.p10k.zsh
2026-04-05 18:24:55 | INFO | [dry-run] write /home/tuxgyver/.p10k.zsh
2026-04-05 18:24:55 | INFO | Ecriture du fichier /home/tuxgyver/.zshrc
2026-04-05 18:24:55 | INFO | [dry-run] write /home/tuxgyver/.zshrc
2026-04-05 18:24:55 | INFO | -> OK (0.1s)
2026-04-05 18:24:55 | INFO | Commande: sudo systemctl enable --now fail2ban.service
2026-04-05 18:24:55 | INFO | [dry-run] sudo systemctl enable --now fail2ban.service
2026-04-05 18:24:55 | INFO | -> OK (0.2s)

View File

@@ -0,0 +1,14 @@
2026-04-05 18:25:36 | INFO | [1/1] Installation et configuration zsh
2026-04-05 18:25:36 | INFO | Commande: sudo apt-get update
2026-04-05 18:25:36 | INFO | [dry-run] sudo apt-get update
2026-04-05 18:25:36 | INFO | Commande: sudo apt-get install -y fonts-powerline
2026-04-05 18:25:36 | INFO | [dry-run] sudo apt-get install -y fonts-powerline
2026-04-05 18:25:36 | INFO | Téléchargement: https://git.h3campus.fr/Johnny/Install_zsh/raw/branch/main/.p10k.zsh
2026-04-05 18:25:36 | INFO | [dry-run] download https://git.h3campus.fr/Johnny/Install_zsh/raw/branch/main/.p10k.zsh
2026-04-05 18:25:36 | INFO | Commande: git clone --depth=1 https://github.com/romkatv/powerlevel10k.git /home/tuxgyver/.powerlevel10k
2026-04-05 18:25:36 | INFO | [dry-run] git clone --depth=1 https://github.com/romkatv/powerlevel10k.git /home/tuxgyver/.powerlevel10k
2026-04-05 18:25:36 | INFO | Ecriture du fichier /home/tuxgyver/.p10k.zsh
2026-04-05 18:25:36 | INFO | [dry-run] write /home/tuxgyver/.p10k.zsh
2026-04-05 18:25:36 | INFO | Ecriture du fichier /home/tuxgyver/.zshrc
2026-04-05 18:25:36 | INFO | [dry-run] write /home/tuxgyver/.zshrc
2026-04-05 18:25:36 | INFO | -> OK (0.1s)

View File

@@ -0,0 +1,14 @@
2026-04-05 18:37:04 | INFO | [1/2] Mises à jour automatiques
2026-04-05 18:37:04 | INFO | Commande: sudo apt-get update
2026-04-05 18:37:04 | INFO | [dry-run] sudo apt-get update
2026-04-05 18:37:04 | INFO | Ecriture du fichier /etc/apt/apt.conf.d/20auto-upgrades
2026-04-05 18:37:04 | INFO | [dry-run] write /etc/apt/apt.conf.d/20auto-upgrades
2026-04-05 18:37:04 | INFO | Ecriture du fichier /etc/apt/apt.conf.d/52securecheck-unattended-upgrades
2026-04-05 18:37:04 | INFO | [dry-run] write /etc/apt/apt.conf.d/52securecheck-unattended-upgrades
2026-04-05 18:37:04 | INFO | Commande: sudo systemctl enable --now unattended-upgrades.service
2026-04-05 18:37:04 | INFO | [dry-run] sudo systemctl enable --now unattended-upgrades.service
2026-04-05 18:37:04 | INFO | -> OK (0.0s)
2026-04-05 18:37:04 | INFO | [2/2] Rotation des logs
2026-04-05 18:37:04 | INFO | Ecriture du fichier /etc/logrotate.d/securecheck
2026-04-05 18:37:04 | INFO | [dry-run] write /etc/logrotate.d/securecheck
2026-04-05 18:37:04 | INFO | -> OK (0.0s)

View File

@@ -0,0 +1,14 @@
2026-04-05 18:37:24 | INFO | [1/2] Mises à jour automatiques
2026-04-05 18:37:24 | INFO | Commande: sudo apt-get update
2026-04-05 18:37:24 | INFO | [dry-run] sudo apt-get update
2026-04-05 18:37:24 | INFO | Ecriture du fichier /etc/apt/apt.conf.d/20auto-upgrades
2026-04-05 18:37:24 | INFO | [dry-run] write /etc/apt/apt.conf.d/20auto-upgrades
2026-04-05 18:37:24 | INFO | Ecriture du fichier /etc/apt/apt.conf.d/52securecheck-unattended-upgrades
2026-04-05 18:37:24 | INFO | [dry-run] write /etc/apt/apt.conf.d/52securecheck-unattended-upgrades
2026-04-05 18:37:24 | INFO | Commande: sudo systemctl enable --now unattended-upgrades.service
2026-04-05 18:37:24 | INFO | [dry-run] sudo systemctl enable --now unattended-upgrades.service
2026-04-05 18:37:24 | INFO | -> OK (0.0s)
2026-04-05 18:37:24 | INFO | [2/2] Rotation des logs
2026-04-05 18:37:24 | INFO | Ecriture du fichier /etc/logrotate.d/securecheck
2026-04-05 18:37:24 | INFO | [dry-run] write /etc/logrotate.d/securecheck
2026-04-05 18:37:24 | INFO | -> OK (0.0s)

View File

@@ -0,0 +1,196 @@
2026-04-05 16:13:09 | INFO | [1/5] Mise à jour système
2026-04-05 16:13:09 | INFO | Commande: sudo apt-get update
2026-04-05 16:13:09 | INFO | [dry-run] sudo apt-get update
2026-04-05 16:13:09 | INFO | Commande: sudo apt-get dist-upgrade -y
2026-04-05 16:13:09 | INFO | [dry-run] sudo apt-get dist-upgrade -y
2026-04-05 16:13:09 | INFO | Commande: sudo apt-get autoremove -y
2026-04-05 16:13:09 | INFO | [dry-run] sudo apt-get autoremove -y
2026-04-05 16:13:09 | INFO | Commande: sudo apt-get autoclean
2026-04-05 16:13:09 | INFO | [dry-run] sudo apt-get autoclean
2026-04-05 16:13:09 | INFO | -> OK (0.0s)
2026-04-05 16:13:09 | INFO | [2/5] Audit Lynis
2026-04-05 16:13:09 | INFO | Commande: sudo lynis audit system --quick
2026-04-05 16:13:09 | INFO | [dry-run] sudo lynis audit system --quick
2026-04-05 16:13:09 | INFO | Ecriture du fichier /home/tuxgyver/scripts/securecheck/.securecheck-runtime/state/logs/reports/20260405-161309-lynis.log
2026-04-05 16:13:09 | INFO | [dry-run] write /home/tuxgyver/scripts/securecheck/.securecheck-runtime/state/logs/reports/20260405-161309-lynis.log
2026-04-05 16:13:09 | INFO | -> OK (0.0s)
2026-04-05 16:13:09 | INFO | [3/5] Vérification rootkits
2026-04-05 16:13:09 | INFO | Commande: sudo apt-get install -y chkrootkit
2026-04-05 16:13:09 | INFO | [dry-run] sudo apt-get install -y chkrootkit
2026-04-05 16:13:09 | INFO | Commande: sudo rkhunter --update
2026-04-05 16:13:09 | INFO | [dry-run] sudo rkhunter --update
2026-04-05 16:13:09 | INFO | Commande: sudo rkhunter --propupd
2026-04-05 16:13:09 | INFO | [dry-run] sudo rkhunter --propupd
2026-04-05 16:13:09 | INFO | Commande: sudo rkhunter --check --skip-keypress --report-warnings-only
2026-04-05 16:13:09 | INFO | [dry-run] sudo rkhunter --check --skip-keypress --report-warnings-only
2026-04-05 16:13:09 | INFO | Commande: sudo chkrootkit -q
2026-04-05 16:13:09 | INFO | [dry-run] sudo chkrootkit -q
2026-04-05 16:13:09 | INFO | Ecriture du fichier /home/tuxgyver/scripts/securecheck/.securecheck-runtime/state/logs/reports/20260405-161309-rootkit-report.json
2026-04-05 16:13:09 | INFO | [dry-run] write /home/tuxgyver/scripts/securecheck/.securecheck-runtime/state/logs/reports/20260405-161309-rootkit-report.json
2026-04-05 16:13:09 | INFO | -> OK (0.0s)
2026-04-05 16:13:09 | INFO | [4/5] Vérification / autoconfig du firewall
2026-04-05 16:13:09 | INFO | Commande: sudo ufw default deny incoming
2026-04-05 16:13:09 | INFO | [dry-run] sudo ufw default deny incoming
2026-04-05 16:13:09 | INFO | Commande: sudo ufw default allow outgoing
2026-04-05 16:13:09 | INFO | [dry-run] sudo ufw default allow outgoing
2026-04-05 16:13:09 | INFO | Commande: sudo ufw status
2026-04-05 16:13:09 | INFO | [dry-run] sudo ufw status
2026-04-05 16:13:09 | INFO | Commande: sudo ufw allow 22/tcp
2026-04-05 16:13:09 | INFO | [dry-run] sudo ufw allow 22/tcp
2026-04-05 16:13:09 | INFO | Commande: sudo ufw --force enable
2026-04-05 16:13:09 | INFO | [dry-run] sudo ufw --force enable
2026-04-05 16:13:09 | INFO | -> OK (0.0s)
2026-04-05 16:13:09 | INFO | [5/5] Rotation des logs
2026-04-05 16:13:09 | INFO | Ecriture du fichier /etc/logrotate.d/securecheck
2026-04-05 16:13:09 | INFO | [dry-run] write /etc/logrotate.d/securecheck
2026-04-05 16:13:09 | INFO | -> OK (0.0s)
2026-04-05 16:13:28 | INFO | [1/5] Mise à jour système
2026-04-05 16:13:28 | INFO | Commande: sudo apt-get update
2026-04-05 16:13:28 | INFO | [dry-run] sudo apt-get update
2026-04-05 16:13:28 | INFO | Commande: sudo apt-get dist-upgrade -y
2026-04-05 16:13:28 | INFO | [dry-run] sudo apt-get dist-upgrade -y
2026-04-05 16:13:28 | INFO | Commande: sudo apt-get autoremove -y
2026-04-05 16:13:28 | INFO | [dry-run] sudo apt-get autoremove -y
2026-04-05 16:13:28 | INFO | Commande: sudo apt-get autoclean
2026-04-05 16:13:28 | INFO | [dry-run] sudo apt-get autoclean
2026-04-05 16:13:28 | INFO | -> OK (0.0s)
2026-04-05 16:13:28 | INFO | [2/5] Audit Lynis
2026-04-05 16:13:28 | INFO | Commande: sudo lynis audit system --quick
2026-04-05 16:13:28 | INFO | [dry-run] sudo lynis audit system --quick
2026-04-05 16:13:28 | INFO | Ecriture du fichier /home/tuxgyver/scripts/securecheck/.securecheck-runtime/state/logs/reports/20260405-161328-lynis.log
2026-04-05 16:13:28 | INFO | [dry-run] write /home/tuxgyver/scripts/securecheck/.securecheck-runtime/state/logs/reports/20260405-161328-lynis.log
2026-04-05 16:13:28 | INFO | -> OK (0.0s)
2026-04-05 16:13:28 | INFO | [3/5] Vérification rootkits
2026-04-05 16:13:28 | INFO | Commande: sudo apt-get install -y chkrootkit
2026-04-05 16:13:28 | INFO | [dry-run] sudo apt-get install -y chkrootkit
2026-04-05 16:13:28 | INFO | Commande: sudo rkhunter --update
2026-04-05 16:13:28 | INFO | [dry-run] sudo rkhunter --update
2026-04-05 16:13:28 | INFO | Commande: sudo rkhunter --propupd
2026-04-05 16:13:28 | INFO | [dry-run] sudo rkhunter --propupd
2026-04-05 16:13:28 | INFO | Commande: sudo rkhunter --check --skip-keypress --report-warnings-only
2026-04-05 16:13:28 | INFO | [dry-run] sudo rkhunter --check --skip-keypress --report-warnings-only
2026-04-05 16:13:28 | INFO | Commande: sudo chkrootkit -q
2026-04-05 16:13:28 | INFO | [dry-run] sudo chkrootkit -q
2026-04-05 16:13:28 | INFO | Ecriture du fichier /home/tuxgyver/scripts/securecheck/.securecheck-runtime/state/logs/reports/20260405-161328-rootkit-report.json
2026-04-05 16:13:28 | INFO | [dry-run] write /home/tuxgyver/scripts/securecheck/.securecheck-runtime/state/logs/reports/20260405-161328-rootkit-report.json
2026-04-05 16:13:28 | INFO | -> OK (0.0s)
2026-04-05 16:13:28 | INFO | [4/5] Vérification / autoconfig du firewall
2026-04-05 16:13:28 | INFO | Commande: sudo ufw default deny incoming
2026-04-05 16:13:28 | INFO | [dry-run] sudo ufw default deny incoming
2026-04-05 16:13:28 | INFO | Commande: sudo ufw default allow outgoing
2026-04-05 16:13:28 | INFO | [dry-run] sudo ufw default allow outgoing
2026-04-05 16:13:28 | INFO | Commande: sudo ufw status
2026-04-05 16:13:28 | INFO | [dry-run] sudo ufw status
2026-04-05 16:13:28 | INFO | Commande: sudo ufw allow 22/tcp
2026-04-05 16:13:28 | INFO | [dry-run] sudo ufw allow 22/tcp
2026-04-05 16:13:28 | INFO | Commande: sudo ufw --force enable
2026-04-05 16:13:28 | INFO | [dry-run] sudo ufw --force enable
2026-04-05 16:13:28 | INFO | -> OK (0.0s)
2026-04-05 16:13:28 | INFO | [5/5] Rotation des logs
2026-04-05 16:13:28 | INFO | Ecriture du fichier /etc/logrotate.d/securecheck
2026-04-05 16:13:28 | INFO | [dry-run] write /etc/logrotate.d/securecheck
2026-04-05 16:13:28 | INFO | -> OK (0.0s)
2026-04-05 18:24:45 | INFO | [1/5] Mise à jour système
2026-04-05 18:24:45 | INFO | Commande: sudo apt-get update
2026-04-05 18:24:45 | INFO | [dry-run] sudo apt-get update
2026-04-05 18:24:45 | INFO | Commande: sudo apt-get dist-upgrade -y
2026-04-05 18:24:45 | INFO | [dry-run] sudo apt-get dist-upgrade -y
2026-04-05 18:24:45 | INFO | Commande: sudo apt-get autoremove -y
2026-04-05 18:24:45 | INFO | [dry-run] sudo apt-get autoremove -y
2026-04-05 18:24:45 | INFO | Commande: sudo apt-get autoclean
2026-04-05 18:24:45 | INFO | [dry-run] sudo apt-get autoclean
2026-04-05 18:24:45 | INFO | -> OK (0.0s)
2026-04-05 18:24:45 | INFO | [2/5] Audit Lynis
2026-04-05 18:24:45 | INFO | Commande: sudo lynis audit system --quick
2026-04-05 18:24:45 | INFO | [dry-run] sudo lynis audit system --quick
2026-04-05 18:24:45 | INFO | Ecriture du fichier /home/tuxgyver/scripts/securecheck/.securecheck-runtime/state/logs/reports/20260405-182445-lynis.log
2026-04-05 18:24:45 | INFO | [dry-run] write /home/tuxgyver/scripts/securecheck/.securecheck-runtime/state/logs/reports/20260405-182445-lynis.log
2026-04-05 18:24:45 | INFO | -> OK (0.0s)
2026-04-05 18:24:45 | INFO | [3/5] Vérification rootkits
2026-04-05 18:24:45 | INFO | Commande: sudo rkhunter --update
2026-04-05 18:24:45 | INFO | [dry-run] sudo rkhunter --update
2026-04-05 18:24:45 | INFO | Commande: sudo rkhunter --propupd
2026-04-05 18:24:45 | INFO | [dry-run] sudo rkhunter --propupd
2026-04-05 18:24:45 | INFO | Commande: sudo rkhunter --check --skip-keypress --report-warnings-only
2026-04-05 18:24:45 | INFO | [dry-run] sudo rkhunter --check --skip-keypress --report-warnings-only
2026-04-05 18:24:45 | INFO | Commande: sudo chkrootkit -q
2026-04-05 18:24:45 | INFO | [dry-run] sudo chkrootkit -q
2026-04-05 18:24:45 | INFO | Ecriture du fichier /home/tuxgyver/scripts/securecheck/.securecheck-runtime/state/logs/reports/20260405-182445-rootkit-report.json
2026-04-05 18:24:45 | INFO | [dry-run] write /home/tuxgyver/scripts/securecheck/.securecheck-runtime/state/logs/reports/20260405-182445-rootkit-report.json
2026-04-05 18:24:45 | INFO | -> OK (0.0s)
2026-04-05 18:24:45 | INFO | [4/5] Vérification / autoconfig du firewall
2026-04-05 18:24:45 | INFO | Commande: sudo ufw default deny incoming
2026-04-05 18:24:45 | INFO | [dry-run] sudo ufw default deny incoming
2026-04-05 18:24:45 | INFO | Commande: sudo ufw default allow outgoing
2026-04-05 18:24:45 | INFO | [dry-run] sudo ufw default allow outgoing
2026-04-05 18:24:45 | INFO | Commande: sudo ufw status
2026-04-05 18:24:45 | INFO | [dry-run] sudo ufw status
2026-04-05 18:24:45 | INFO | Commande: sudo ufw allow 22/tcp
2026-04-05 18:24:45 | INFO | [dry-run] sudo ufw allow 22/tcp
2026-04-05 18:24:45 | INFO | Commande: sudo ufw --force enable
2026-04-05 18:24:45 | INFO | [dry-run] sudo ufw --force enable
2026-04-05 18:24:45 | INFO | -> OK (0.0s)
2026-04-05 18:24:45 | INFO | [5/5] Rotation des logs
2026-04-05 18:24:45 | INFO | Ecriture du fichier /etc/logrotate.d/securecheck
2026-04-05 18:24:45 | INFO | [dry-run] write /etc/logrotate.d/securecheck
2026-04-05 18:24:45 | INFO | -> OK (0.0s)
2026-04-05 18:24:55 | INFO | [1/1] Installation et configuration zsh
2026-04-05 18:24:55 | INFO | Commande: sudo apt-get update
2026-04-05 18:24:55 | INFO | [dry-run] sudo apt-get update
2026-04-05 18:24:55 | INFO | [1/1] Utilitaires pratiques
2026-04-05 18:24:55 | INFO | Commande: sudo apt-get update
2026-04-05 18:24:55 | INFO | [dry-run] sudo apt-get update
2026-04-05 18:24:55 | INFO | Commande: sudo apt-get install -y fonts-powerline
2026-04-05 18:24:55 | INFO | [dry-run] sudo apt-get install -y fonts-powerline
2026-04-05 18:24:55 | INFO | Téléchargement: https://git.h3campus.fr/Johnny/Install_zsh/raw/branch/main/.p10k.zsh
2026-04-05 18:24:55 | INFO | [dry-run] download https://git.h3campus.fr/Johnny/Install_zsh/raw/branch/main/.p10k.zsh
2026-04-05 18:24:55 | INFO | Ecriture du fichier /home/tuxgyver/.p10k.zsh
2026-04-05 18:24:55 | INFO | [dry-run] write /home/tuxgyver/.p10k.zsh
2026-04-05 18:24:55 | INFO | Ecriture du fichier /home/tuxgyver/.zshrc
2026-04-05 18:24:55 | INFO | [dry-run] write /home/tuxgyver/.zshrc
2026-04-05 18:24:55 | INFO | -> OK (0.1s)
2026-04-05 18:24:55 | INFO | Commande: sudo systemctl enable --now fail2ban.service
2026-04-05 18:24:55 | INFO | [dry-run] sudo systemctl enable --now fail2ban.service
2026-04-05 18:24:55 | INFO | -> OK (0.2s)
2026-04-05 18:25:36 | INFO | [1/1] Installation et configuration zsh
2026-04-05 18:25:36 | INFO | Commande: sudo apt-get update
2026-04-05 18:25:36 | INFO | [dry-run] sudo apt-get update
2026-04-05 18:25:36 | INFO | Commande: sudo apt-get install -y fonts-powerline
2026-04-05 18:25:36 | INFO | [dry-run] sudo apt-get install -y fonts-powerline
2026-04-05 18:25:36 | INFO | Téléchargement: https://git.h3campus.fr/Johnny/Install_zsh/raw/branch/main/.p10k.zsh
2026-04-05 18:25:36 | INFO | [dry-run] download https://git.h3campus.fr/Johnny/Install_zsh/raw/branch/main/.p10k.zsh
2026-04-05 18:25:36 | INFO | Commande: git clone --depth=1 https://github.com/romkatv/powerlevel10k.git /home/tuxgyver/.powerlevel10k
2026-04-05 18:25:36 | INFO | [dry-run] git clone --depth=1 https://github.com/romkatv/powerlevel10k.git /home/tuxgyver/.powerlevel10k
2026-04-05 18:25:36 | INFO | Ecriture du fichier /home/tuxgyver/.p10k.zsh
2026-04-05 18:25:36 | INFO | [dry-run] write /home/tuxgyver/.p10k.zsh
2026-04-05 18:25:36 | INFO | Ecriture du fichier /home/tuxgyver/.zshrc
2026-04-05 18:25:36 | INFO | [dry-run] write /home/tuxgyver/.zshrc
2026-04-05 18:25:36 | INFO | -> OK (0.1s)
2026-04-05 18:37:04 | INFO | [1/2] Mises à jour automatiques
2026-04-05 18:37:04 | INFO | Commande: sudo apt-get update
2026-04-05 18:37:04 | INFO | [dry-run] sudo apt-get update
2026-04-05 18:37:04 | INFO | Ecriture du fichier /etc/apt/apt.conf.d/20auto-upgrades
2026-04-05 18:37:04 | INFO | [dry-run] write /etc/apt/apt.conf.d/20auto-upgrades
2026-04-05 18:37:04 | INFO | Ecriture du fichier /etc/apt/apt.conf.d/52securecheck-unattended-upgrades
2026-04-05 18:37:04 | INFO | [dry-run] write /etc/apt/apt.conf.d/52securecheck-unattended-upgrades
2026-04-05 18:37:04 | INFO | Commande: sudo systemctl enable --now unattended-upgrades.service
2026-04-05 18:37:04 | INFO | [dry-run] sudo systemctl enable --now unattended-upgrades.service
2026-04-05 18:37:04 | INFO | -> OK (0.0s)
2026-04-05 18:37:04 | INFO | [2/2] Rotation des logs
2026-04-05 18:37:04 | INFO | Ecriture du fichier /etc/logrotate.d/securecheck
2026-04-05 18:37:04 | INFO | [dry-run] write /etc/logrotate.d/securecheck
2026-04-05 18:37:04 | INFO | -> OK (0.0s)
2026-04-05 18:37:24 | INFO | [1/2] Mises à jour automatiques
2026-04-05 18:37:24 | INFO | Commande: sudo apt-get update
2026-04-05 18:37:24 | INFO | [dry-run] sudo apt-get update
2026-04-05 18:37:24 | INFO | Ecriture du fichier /etc/apt/apt.conf.d/20auto-upgrades
2026-04-05 18:37:24 | INFO | [dry-run] write /etc/apt/apt.conf.d/20auto-upgrades
2026-04-05 18:37:24 | INFO | Ecriture du fichier /etc/apt/apt.conf.d/52securecheck-unattended-upgrades
2026-04-05 18:37:24 | INFO | [dry-run] write /etc/apt/apt.conf.d/52securecheck-unattended-upgrades
2026-04-05 18:37:24 | INFO | Commande: sudo systemctl enable --now unattended-upgrades.service
2026-04-05 18:37:24 | INFO | [dry-run] sudo systemctl enable --now unattended-upgrades.service
2026-04-05 18:37:24 | INFO | -> OK (0.0s)
2026-04-05 18:37:24 | INFO | [2/2] Rotation des logs
2026-04-05 18:37:24 | INFO | Ecriture du fichier /etc/logrotate.d/securecheck
2026-04-05 18:37:24 | INFO | [dry-run] write /etc/logrotate.d/securecheck
2026-04-05 18:37:24 | INFO | -> OK (0.0s)

View File

@@ -67,6 +67,27 @@ python3 -m securecheck --scenario baseline_workstation --run
python3 -m securecheck --scenario baseline_workstation
```
## Build d'un exécutable
La cible est un binaire autonome via PyInstaller. Exemple complet :
1. Crée un environnement propre (obligatoire dans cet environnement verrouillé) :
```bash
python3 -m venv .venv
.venv/bin/pip install --upgrade pip
.venv/bin/pip install pyinstaller
```
2. Lance le script de construction :
```bash
./build_executable.sh
```
Il appelle PyInstaller avec `--onefile` et embarque `securecheck/assets`.
3. Le résultat est dans `dist/securecheck` (et `build/` + `securecheck.spec`). Supprime `dist/ build/ securecheck.spec` si tu reconstruis.
> Si PyInstaller ne peut pas être téléchargé (pas de réseau), installe-le via `apt install pyinstaller` ou télécharge-le manuellement avant de relancer le script.
## Emplacements
- Scénarios : `~/.config/securecheck/scenarios.json`

18
build_executable.sh Executable file
View File

@@ -0,0 +1,18 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ ! -d ".venv" ]]; then
echo ".venv absent. Crée-le avec python3 -m venv .venv avant."
exit 1
fi
source .venv/bin/activate
pyinstaller \
--onefile \
--name securecheck \
--add-data "securecheck/assets:securecheck/assets" \
--hidden-import pkg_resources.py2_warn \
securecheck/__main__.py
echo "Binaire généré dans dist/securecheck"

27
pyproject.toml Normal file
View File

@@ -0,0 +1,27 @@
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
[project]
name = "securecheck"
version = "0.1.0"
description = "Application console semi-graphique pour automatiser des contrôles et durcissements Linux."
readme = "README.md"
requires-python = ">=3.11"
authors = [
{ name = "Codex" }
]
license = { text = "MIT" }
dependencies = []
[project.scripts]
securecheck = "securecheck.__main__:main"
[tool.setuptools]
include-package-data = true
[tool.setuptools.packages.find]
where = ["."]
[tool.setuptools.package-data]
securecheck = ["assets/*"]

5
securecheck/__init__.py Normal file
View File

@@ -0,0 +1,5 @@
"""SecureCheck package."""
__all__ = ["__version__"]
__version__ = "0.1.0"

137
securecheck/__main__.py Normal file
View File

@@ -0,0 +1,137 @@
from __future__ import annotations
import argparse
import sys
from datetime import datetime
from .app import RunSummaryTUI, SecureCheckTUI
from .catalog import builtin_scenarios, task_catalog
from .config import build_paths, ensure_app_dirs
from .executor import ExecutionContext, execute_tasks
from .logging_utils import attach_run_handler, setup_logging
from .status import collect_status
from .storage import ScenarioStore
from .system_info import detect_system
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")
parser.add_argument("--run", action="store_true", help="Lance immédiatement les tâches passées via --tasks ou --scenario")
parser.add_argument("--tasks", help="Liste de tâches séparées par des virgules")
parser.add_argument("--scenario", help="Nom d'un scénario enregistré ou builtin")
parser.add_argument("--list-scenarios", action="store_true", help="Affiche les scénarios disponibles")
return parser.parse_args()
def resolve_task_selection(args: argparse.Namespace, store: ScenarioStore, available_task_keys: set[str]) -> list[str]:
if args.scenario:
scenario = store.get(args.scenario)
if not scenario:
raise SystemExit(f"Scénario inconnu: {args.scenario}")
return [key for key in scenario.task_keys if key in available_task_keys]
if args.tasks:
selected = [key.strip() for key in args.tasks.split(",") if key.strip()]
invalid = [key for key in selected if key not in available_task_keys]
if invalid:
raise SystemExit(f"Tâches inconnues: {', '.join(invalid)}")
return selected
return []
def print_led_dashboard(system) -> None:
print("")
print("=== Etat du système ===")
for item in collect_status(system):
led = "\033[32m●\033[0m" if item.ok else "\033[31m●\033[0m"
print(f"{led} [{item.category}] {item.label}: {item.detail}")
def print_summary(results, run_log_path, system) -> None:
ok_count = sum(1 for result in results if result.success)
ko_count = len(results) - ok_count
print("")
print("=== Résumé ===")
for result in results:
status = "OK" if result.success else "ECHEC"
suffix = f" | erreur: {result.error}" if result.error else ""
print(f"- {status} | {result.label} | {result.duration_seconds:.1f}s{suffix}")
for detail in result.details:
print(f" {detail}")
print_led_dashboard(system)
print(f"Logs d'exécution: {run_log_path}")
print(f"Total OK={ok_count} / ECHEC={ko_count}")
def main() -> int:
args = parse_args()
paths = build_paths()
ensure_app_dirs(paths)
logger = setup_logging(paths.app_log_file)
system = detect_system()
tasks = task_catalog()
task_by_key = {task.key: task for task in tasks}
store = ScenarioStore(paths.scenario_file, builtin_scenarios())
if args.list_scenarios:
for scenario in store.list_all():
print(f"{scenario.name}: {scenario.description}")
return 0
selected_keys = resolve_task_selection(args, store, set(task_by_key))
interactive_mode = not args.run
active_scenario_name = args.scenario
menu_message: str | None = None
while True:
if interactive_mode:
tui = SecureCheckTUI(
system,
tasks,
store,
status_provider=lambda: collect_status(system),
initial_selected=set(selected_keys) if selected_keys else None,
initial_scenario_name=active_scenario_name,
initial_message=menu_message,
)
selection = tui.run()
if selection is None:
return 0
selected_keys = selection.task_keys
active_scenario_name = selection.scenario_name
if not selected_keys:
if interactive_mode:
menu_message = "Aucune tâche sélectionnée."
continue
print("Aucune tâche sélectionnée.")
return 1
selected_tasks = [task_by_key[key] for key in selected_keys]
context = ExecutionContext(paths=paths, system=system, logger=logger, dry_run=args.dry_run)
run_log_path = paths.log_dir / f"run-{datetime.now().strftime('%Y%m%d-%H%M%S')}.log"
run_handler = attach_run_handler(logger, run_log_path)
try:
results = execute_tasks(context, selected_tasks)
finally:
logger.removeHandler(run_handler)
run_handler.close()
if interactive_mode:
status_items = collect_status(system)
RunSummaryTUI(results, status_items, str(run_log_path)).run()
ok_count = sum(1 for result in results if result.success)
ko_count = len(results) - ok_count
menu_message = f"Dernière exécution: {ok_count} OK / {ko_count} ECHEC. Sélection prête pour une nouvelle action."
continue
print_summary(results, run_log_path, system)
return 0 if all(result.success for result in results) else 2
if __name__ == "__main__":
sys.exit(main())

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

401
securecheck/app.py Normal file
View File

@@ -0,0 +1,401 @@
from __future__ import annotations
import curses
import textwrap
from collections import defaultdict
from dataclasses import dataclass
from typing import Callable
from .assets import banner_text
from .models import Scenario, TaskDefinition, TaskResult
from .status import StatusItem
from .storage import ScenarioStore
from .system_info import SystemInfo
@dataclass
class AppSelection:
task_keys: list[str]
scenario_name: str | None = None
class Palette:
TITLE = 1
HEADER = 2
PANEL = 3
SELECTED = 4
SUCCESS = 5
ERROR = 6
MUTED = 7
HIGHLIGHT = 8
CATEGORY = 9
def _setup_colors() -> None:
if not curses.has_colors():
return
curses.start_color()
curses.use_default_colors()
curses.init_pair(Palette.TITLE, curses.COLOR_CYAN, -1)
curses.init_pair(Palette.HEADER, curses.COLOR_YELLOW, -1)
curses.init_pair(Palette.PANEL, curses.COLOR_WHITE, curses.COLOR_BLUE)
curses.init_pair(Palette.SELECTED, curses.COLOR_BLACK, curses.COLOR_CYAN)
curses.init_pair(Palette.SUCCESS, curses.COLOR_GREEN, -1)
curses.init_pair(Palette.ERROR, curses.COLOR_RED, -1)
curses.init_pair(Palette.MUTED, curses.COLOR_BLUE, -1)
curses.init_pair(Palette.HIGHLIGHT, curses.COLOR_BLACK, curses.COLOR_YELLOW)
curses.init_pair(Palette.CATEGORY, curses.COLOR_MAGENTA, -1)
class SecureCheckTUI:
def __init__(
self,
system: SystemInfo,
tasks: list[TaskDefinition],
store: ScenarioStore,
*,
status_provider: Callable[[], list[StatusItem]],
initial_selected: set[str] | None = None,
initial_scenario_name: str | None = None,
initial_message: str | None = None,
) -> None:
self.system = system
self.tasks = tasks
self.store = store
self.status_provider = status_provider
self.index = 0
self.message = initial_message or "Sélectionnez les tâches puis lancez avec 'r'."
self.selected = initial_selected if initial_selected is not None else {task.key for task in tasks if task.default_selected}
self.scenario_name = initial_scenario_name
self.banner_lines = banner_text().splitlines()
self.status_items = self.status_provider()
def run(self) -> AppSelection | None:
return curses.wrapper(self._main)
def _main(self, stdscr: curses.window) -> AppSelection | None:
curses.curs_set(0)
stdscr.keypad(True)
_setup_colors()
while True:
self._draw(stdscr)
key = stdscr.getch()
if key in (ord("q"), 27):
return None
if key in (curses.KEY_UP, ord("k")):
self.index = max(0, self.index - 1)
elif key in (curses.KEY_DOWN, ord("j")):
self.index = min(len(self.tasks) - 1, self.index + 1)
elif key == ord(" "):
current = self.tasks[self.index].key
if current in self.selected:
self.selected.remove(current)
else:
self.selected.add(current)
elif key == ord("a"):
baseline = self.store.get("baseline_workstation")
if baseline:
self.selected = set(baseline.task_keys)
self.scenario_name = baseline.name
self.message = "Scénario baseline_workstation chargé."
elif key == ord("s"):
self._save_current(stdscr)
elif key == ord("l"):
self._load_scenario(stdscr)
elif key == ord("d"):
self._show_dashboard(stdscr)
elif key == ord("x"):
self._delete_scenario(stdscr)
elif key == ord("r"):
if not self.selected:
self.message = "Aucune tâche sélectionnée."
continue
return AppSelection(
task_keys=[task.key for task in self.tasks if task.key in self.selected],
scenario_name=self.scenario_name,
)
elif key == ord("?"):
self._show_help(stdscr)
def _draw(self, stdscr: curses.window) -> None:
stdscr.erase()
height, width = stdscr.getmaxyx()
header_row = 1
for line in self.banner_lines[: min(6, max(0, height - 8))]:
stdscr.addnstr(header_row, 2, line, width - 4, curses.color_pair(Palette.TITLE) | curses.A_BOLD)
header_row += 1
scenario_hint = f" | scénario={self.scenario_name}" if self.scenario_name else ""
header = f"SecureCheck | {self.system.pretty_name} | user={self.system.target_user} | pkg={self.system.package_manager}{scenario_hint}"
stdscr.addnstr(header_row, 2, header, width - 4, curses.color_pair(Palette.HEADER) | curses.A_BOLD)
stdscr.addnstr(
header_row + 1,
2,
"↑↓ naviguer Espace cocher s sauver l charger d état x supprimer r exécuter q quitter",
width - 4,
curses.color_pair(Palette.MUTED),
)
stdscr.hline(header_row + 2, 1, curses.ACS_HLINE, width - 2)
list_top = header_row + 4
desc_height = 5
status_width = 40 if width >= 120 else 0
task_width = width - 4 - status_width - (2 if status_width else 0)
left_x = 1
right_x = left_x + task_width + 2 if status_width else 0
content_height = max(8, height - list_top - desc_height - 2)
self._draw_box(stdscr, list_top, left_x, content_height, task_width, "Tâches")
self._draw_task_list(stdscr, list_top + 1, left_x + 1, content_height - 2, task_width - 2)
if status_width:
self._draw_box(stdscr, list_top, right_x, content_height, status_width, "Etat")
self._draw_status_panel(stdscr, list_top + 1, right_x + 1, content_height - 2, status_width - 2)
desc_top = list_top + content_height + 1
self._draw_box(stdscr, desc_top, 1, desc_height, width - 2, "Détail")
current = self.tasks[self.index]
count_selected = len(self.selected)
total_ok = sum(1 for item in self.status_items if item.ok)
total_ko = len(self.status_items) - total_ok
summary = f"Sélection: {count_selected}/{len(self.tasks)} | Etat: {total_ok} OK / {total_ko} KO"
stdscr.addnstr(desc_top + 1, 3, summary, width - 8, curses.color_pair(Palette.HEADER) | curses.A_BOLD)
for offset, line in enumerate(textwrap.wrap(current.description, width - 8)[:2], start=desc_top + 2):
stdscr.addnstr(offset, 3, line, width - 8)
stdscr.addnstr(height - 1, 2, self.message, width - 4, curses.color_pair(Palette.MUTED) | curses.A_BOLD)
stdscr.refresh()
def _draw_box(self, stdscr: curses.window, top: int, left: int, height: int, width: int, title: str) -> None:
stdscr.attron(curses.color_pair(Palette.PANEL))
stdscr.addch(top, left, curses.ACS_ULCORNER)
stdscr.hline(top, left + 1, curses.ACS_HLINE, width - 2)
stdscr.addch(top, left + width - 1, curses.ACS_URCORNER)
for row in range(top + 1, top + height - 1):
stdscr.addch(row, left, curses.ACS_VLINE)
stdscr.addch(row, left + width - 1, curses.ACS_VLINE)
stdscr.addch(top + height - 1, left, curses.ACS_LLCORNER)
stdscr.hline(top + height - 1, left + 1, curses.ACS_HLINE, width - 2)
stdscr.addch(top + height - 1, left + width - 1, curses.ACS_LRCORNER)
stdscr.attroff(curses.color_pair(Palette.PANEL))
stdscr.addnstr(top, left + 2, f" {title} ", width - 4, curses.color_pair(Palette.HEADER) | curses.A_BOLD)
def _draw_task_list(self, stdscr: curses.window, top: int, left: int, height: int, width: int) -> None:
window_start = max(0, min(self.index - (height // 2), max(0, len(self.tasks) - height)))
visible_tasks = self.tasks[window_start : window_start + height]
for offset in range(height):
stdscr.addnstr(top + offset, left, " " * width, width)
for row, task in enumerate(visible_tasks):
y = top + row
selected = task.key in self.selected
current = self.tasks[self.index].key == task.key
marker = "" if selected else " "
category = f"[{task.category}]"
base_attr = curses.color_pair(Palette.SELECTED) | curses.A_BOLD if current else curses.A_NORMAL
stdscr.addnstr(y, left, " " * width, width, base_attr)
led_attr = curses.color_pair(Palette.SUCCESS if selected else Palette.MUTED) | curses.A_BOLD
stdscr.addnstr(y, left, "", 1, led_attr | (curses.A_REVERSE if current else 0))
stdscr.addnstr(y, left + 2, f"[{marker}]", 3, base_attr | (curses.color_pair(Palette.SUCCESS) if selected else 0))
stdscr.addnstr(y, left + 6, task.label, max(1, width - 22), base_attr)
stdscr.addnstr(y, left + width - min(18, len(category) + 1), category, min(18, width - 1), curses.color_pair(Palette.CATEGORY) | (curses.A_BOLD if current else 0))
def _draw_status_panel(self, stdscr: curses.window, top: int, left: int, height: int, width: int) -> None:
self.status_items = self.status_provider()
items = self.status_items[:height]
for offset in range(height):
stdscr.addnstr(top + offset, left, " " * width, width)
for row, item in enumerate(items):
color = curses.color_pair(Palette.SUCCESS if item.ok else Palette.ERROR) | curses.A_BOLD
stdscr.addnstr(top + row, left, "", 1, color)
line = f" {item.label:<16} {item.detail}"
stdscr.addnstr(top + row, left + 2, line, width - 2)
def _prompt(self, stdscr: curses.window, prompt: str) -> str | None:
height, width = stdscr.getmaxyx()
curses.echo()
curses.curs_set(1)
stdscr.move(height - 1, 0)
stdscr.clrtoeol()
stdscr.addnstr(height - 1, 2, prompt, width - 4, curses.color_pair(Palette.HIGHLIGHT) | curses.A_BOLD)
value = stdscr.getstr(height - 1, min(len(prompt) + 2, width - 2), 60).decode("utf-8").strip()
curses.noecho()
curses.curs_set(0)
return value or None
def _pick_scenario(self, stdscr: curses.window, *, deletable_only: bool = False) -> Scenario | None:
scenarios = self.store.list_all()
if deletable_only:
scenarios = [scenario for scenario in scenarios if not scenario.builtin]
if not scenarios:
self.message = "Aucun scénario disponible."
return None
index = 0
while True:
stdscr.erase()
_setup_colors()
height, width = stdscr.getmaxyx()
self._draw_box(stdscr, 0, 1, height - 1, width - 2, "Scénarios")
stdscr.addnstr(1, 3, "Entrée valider | q retour", width - 6, curses.color_pair(Palette.MUTED))
for line_no, scenario in enumerate(scenarios[: height - 4], start=2):
prefix = ">" if line_no - 2 == index else " "
kind = "builtin" if scenario.builtin else "user"
attr = curses.color_pair(Palette.SELECTED) | curses.A_BOLD if line_no - 2 == index else curses.A_NORMAL
stdscr.addnstr(line_no + 1, 3, f"{prefix} {scenario.name} [{kind}] - {scenario.description}", width - 6, attr)
stdscr.refresh()
key = stdscr.getch()
if key in (ord("q"), 27):
return None
if key in (curses.KEY_UP, ord("k")):
index = max(0, index - 1)
elif key in (curses.KEY_DOWN, ord("j")):
index = min(len(scenarios) - 1, index + 1)
elif key in (10, 13, curses.KEY_ENTER):
return scenarios[index]
def _save_current(self, stdscr: curses.window) -> None:
name = self._prompt(stdscr, "Nom du scénario: ")
if not name:
self.message = "Sauvegarde annulée."
return
description = self._prompt(stdscr, "Description courte: ") or ""
self.store.save(Scenario(name=name, description=description, task_keys=sorted(self.selected)))
self.scenario_name = name
self.message = f"Scénario '{name}' enregistré."
def _load_scenario(self, stdscr: curses.window) -> None:
scenario = self._pick_scenario(stdscr)
if not scenario:
self.message = "Chargement annulé."
return
self.selected = set(scenario.task_keys)
self.scenario_name = scenario.name
self.message = f"Scénario '{scenario.name}' chargé."
def _delete_scenario(self, stdscr: curses.window) -> None:
scenario = self._pick_scenario(stdscr, deletable_only=True)
if not scenario:
self.message = "Suppression annulée."
return
if self.store.delete(scenario.name):
self.message = f"Scénario '{scenario.name}' supprimé."
else:
self.message = "Impossible de supprimer le scénario."
def _show_help(self, stdscr: curses.window) -> None:
lines = [
"SecureCheck - Aide rapide",
"",
"Chaque ligne correspond à une action automatisable.",
"Les scénarios permettent d'enregistrer un lot réutilisable.",
"Vous pouvez précharger un scénario puis ajuster les cases avant exécution.",
"La touche d affiche le tableau d'état du système avec indicateurs rouge/vert.",
"Après exécution, l'application affiche un résumé puis revient au menu.",
"",
"Appuyez sur une touche pour revenir.",
]
stdscr.erase()
_setup_colors()
height, width = stdscr.getmaxyx()
self._draw_box(stdscr, 0, 1, height - 1, width - 2, "Aide")
for index, line in enumerate(lines[: height - 1]):
stdscr.addnstr(index + 1, 3, line, width - 6, curses.color_pair(Palette.HEADER) if index == 0 else curses.A_NORMAL)
stdscr.refresh()
stdscr.getch()
def _show_dashboard(self, stdscr: curses.window) -> None:
self.status_items = self.status_provider()
groups: dict[str, list[StatusItem]] = defaultdict(list)
for item in self.status_items:
groups[item.category].append(item)
_setup_colors()
stdscr.erase()
height, width = stdscr.getmaxyx()
self._draw_box(stdscr, 0, 1, height - 1, width - 2, "Tableau d'état")
stdscr.addnstr(1, 3, "Vert = OK | Rouge = manquant/inactif | Appuyez sur une touche", width - 6, curses.color_pair(Palette.MUTED))
row = 3
for category, items in groups.items():
if row >= height - 1:
break
stdscr.addnstr(row, 3, f"[{category}]", width - 6, curses.color_pair(Palette.CATEGORY) | curses.A_BOLD)
row += 1
for item in items:
if row >= height - 1:
break
color = curses.color_pair(Palette.SUCCESS if item.ok else Palette.ERROR)
stdscr.addstr(row, 3, "", color | curses.A_BOLD)
stdscr.addnstr(row, 5, f"{item.label:<18} {item.detail}", width - 8)
row += 1
row += 1
stdscr.refresh()
stdscr.getch()
class RunSummaryTUI:
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
def run(self) -> None:
curses.wrapper(self._main)
def _main(self, stdscr: curses.window) -> None:
curses.curs_set(0)
stdscr.keypad(True)
stdscr.timeout(5000)
_setup_colors()
while True:
self._draw(stdscr)
key = stdscr.getch()
if key == -1 or key in (ord("q"), 27, 10, 13, ord("m"), ord(" ")):
return
def _draw(self, stdscr: curses.window) -> None:
stdscr.erase()
height, width = stdscr.getmaxyx()
ok_count = sum(1 for result in self.results if result.success)
ko_count = len(self.results) - ok_count
self._draw_box(stdscr, 0, 1, height - 1, width - 2, "Résumé d'exécution")
stdscr.addnstr(1, 3, f"OK: {ok_count} | ECHEC: {ko_count} | Retour menu auto dans 5s", width - 6, curses.color_pair(Palette.HEADER) | curses.A_BOLD)
row = 3
for result in self.results:
if row >= height - 6:
break
color = curses.color_pair(Palette.SUCCESS if result.success else Palette.ERROR)
status = "OK" if result.success else "ECHEC"
line = f"{status:<5} {result.label} ({result.duration_seconds:.1f}s)"
stdscr.addnstr(row, 3, line, width - 6, color | curses.A_BOLD)
row += 1
for detail in result.details[:2]:
if row >= height - 6:
break
stdscr.addnstr(row, 6, f"- {detail}", width - 9)
row += 1
if result.error and row < height - 6:
stdscr.addnstr(row, 6, f"- {result.error}", width - 9, curses.color_pair(Palette.ERROR))
row += 1
row += 1
stdscr.addnstr(row, 3, "Etat synthétique:", width - 6, curses.color_pair(Palette.CATEGORY) | curses.A_BOLD)
row += 1
for item in self.status_items[: max(0, height - row - 2)]:
color = curses.color_pair(Palette.SUCCESS if item.ok else Palette.ERROR)
stdscr.addnstr(row, 3, "", 1, color | curses.A_BOLD)
stdscr.addnstr(row, 5, f"[{item.category}] {item.label}: {item.detail}", width - 8)
row += 1
stdscr.addnstr(height - 2, 3, f"Log: {self.run_log_path}", width - 6, curses.color_pair(Palette.MUTED))
def _draw_box(self, stdscr: curses.window, top: int, left: int, height: int, width: int, title: str) -> None:
stdscr.attron(curses.color_pair(Palette.PANEL))
stdscr.addch(top, left, curses.ACS_ULCORNER)
stdscr.hline(top, left + 1, curses.ACS_HLINE, width - 2)
stdscr.addch(top, left + width - 1, curses.ACS_URCORNER)
for row in range(top + 1, top + height - 1):
stdscr.addch(row, left, curses.ACS_VLINE)
stdscr.addch(row, left + width - 1, curses.ACS_VLINE)
stdscr.addch(top + height - 1, left, curses.ACS_LLCORNER)
stdscr.hline(top + height - 1, left + 1, curses.ACS_HLINE, width - 2)
stdscr.addch(top + height - 1, left + width - 1, curses.ACS_LRCORNER)
stdscr.attroff(curses.color_pair(Palette.PANEL))
stdscr.addnstr(top, left + 2, f" {title} ", width - 4, curses.color_pair(Palette.HEADER) | curses.A_BOLD)

15
securecheck/assets.py Normal file
View File

@@ -0,0 +1,15 @@
from __future__ import annotations
from importlib.resources import files
def asset_path(name: str) -> str:
return str(files("securecheck").joinpath("assets", name))
def banner_text() -> str:
return files("securecheck").joinpath("assets", "banner.txt").read_text(encoding="utf-8")
def asset_text(name: str) -> str:
return files("securecheck").joinpath("assets", name).read_text(encoding="utf-8")

View File

@@ -0,0 +1,6 @@
_____ _____ _ _
/ ____| / ____| | | |
| (___ ___ ___ _ _ _ __ ___| | | |__ ___ ___| | __
\___ \ / _ \/ __| | | | '__/ _ \ | | '_ \ / _ \/ __| |/ /
____) | __/ (__| |_| | | | __/ |____| | | | __/ (__| <
|_____/ \___|\___|\__,_|_| \___|\_____|_| |_|\___|\___|_|\_\

1840
securecheck/assets/p10k.zsh Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -0,0 +1,31 @@
<svg width="256" height="256" viewBox="0 0 256 256" fill="none" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="bg" x1="32" y1="28" x2="224" y2="228" gradientUnits="userSpaceOnUse">
<stop stop-color="#0F172A"/>
<stop offset="1" stop-color="#1E293B"/>
</linearGradient>
<linearGradient id="shield" x1="128" y1="48" x2="128" y2="208" gradientUnits="userSpaceOnUse">
<stop stop-color="#DFF8F2"/>
<stop offset="1" stop-color="#A7F3D0"/>
</linearGradient>
<linearGradient id="accent" x1="72" y1="76" x2="188" y2="188" gradientUnits="userSpaceOnUse">
<stop stop-color="#2DD4BF"/>
<stop offset="1" stop-color="#14B8A6"/>
</linearGradient>
</defs>
<rect x="20" y="20" width="216" height="216" rx="48" fill="url(#bg)"/>
<rect x="28" y="28" width="200" height="200" rx="40" stroke="#334155" stroke-width="2"/>
<path d="M128 48L184 70V116C184 154 162 189 128 208C94 189 72 154 72 116V70L128 48Z" fill="url(#shield)"/>
<path d="M128 58L176 76V114C176 148 157 179 128 196C99 179 80 148 80 114V76L128 58Z" fill="url(#accent)"/>
<rect x="92" y="92" width="72" height="52" rx="12" fill="#0F172A"/>
<path d="M104 107L117 118L104 129" stroke="#F8FAFC" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M124 128H150" stroke="#F59E0B" stroke-width="8" stroke-linecap="round"/>
<path d="M97 154L117 174L160 131" stroke="#F8FAFC" stroke-width="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M184 92C194 103 200 117 200 132" stroke="#67E8F9" stroke-width="6" stroke-linecap="round" opacity="0.7"/>
<path d="M56 132C56 117 62 103 72 92" stroke="#67E8F9" stroke-width="6" stroke-linecap="round" opacity="0.35"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

158
securecheck/catalog.py Normal file
View File

@@ -0,0 +1,158 @@
from __future__ import annotations
from .models import Scenario, TaskDefinition
from .tasks import (
automatic_updates,
bind,
docker_setup,
firewall_setup,
log_rotation,
lynis_audit,
rootkit_check,
system_update,
utilities_setup,
zram_setup,
zsh_setup,
)
def task_catalog() -> list[TaskDefinition]:
base = [
TaskDefinition(
key="system_update",
label="Mise à jour système",
description="Met à jour le système et nettoie les paquets obsolètes.",
category="Maintenance",
handler=lambda context: None, # replaced by bind()
default_selected=True,
),
TaskDefinition(
key="automatic_updates",
label="Mises à jour automatiques",
description="Configure unattended-upgrades ou dnf-automatic.",
category="Maintenance",
handler=lambda context: None,
default_selected=True,
),
TaskDefinition(
key="lynis_audit",
label="Audit Lynis",
description="Lance un audit sécurité automatisé avec Lynis.",
category="Sécurité",
handler=lambda context: None,
default_selected=True,
),
TaskDefinition(
key="rootkit_check",
label="Vérification rootkits",
description="Exécute rkhunter et chkrootkit.",
category="Sécurité",
handler=lambda context: None,
default_selected=True,
),
TaskDefinition(
key="log_rotation",
label="Rotation des logs",
description="Installe et configure logrotate pour SecureCheck.",
category="Maintenance",
handler=lambda context: None,
default_selected=True,
),
TaskDefinition(
key="zsh_setup",
label="Installation et configuration zsh",
description="Installe zsh et applique une configuration utilisateur propre.",
category="Poste",
handler=lambda context: None,
default_selected=True,
),
TaskDefinition(
key="utilities_setup",
label="Utilitaires pratiques",
description="Installe les outils usuels de maintenance et sécurité.",
category="Poste",
handler=lambda context: None,
default_selected=True,
),
TaskDefinition(
key="zram_setup",
label="zram auto-configuré",
description="Déploie un service zram dimensionné automatiquement.",
category="Performance",
handler=lambda context: None,
default_selected=True,
),
TaskDefinition(
key="firewall_setup",
label="Vérification / autoconfig du firewall",
description="Active et sécurise UFW ou firewalld.",
category="Sécurité",
handler=lambda context: None,
default_selected=True,
),
TaskDefinition(
key="docker_setup",
label="Installation / check Docker",
description="Installe Docker et configure la rotation de ses logs.",
category="Services",
handler=lambda context: None,
default_selected=False,
),
]
handlers = {
"system_update": system_update,
"automatic_updates": automatic_updates,
"lynis_audit": lynis_audit,
"rootkit_check": rootkit_check,
"log_rotation": log_rotation,
"zsh_setup": zsh_setup,
"utilities_setup": utilities_setup,
"zram_setup": zram_setup,
"firewall_setup": firewall_setup,
"docker_setup": docker_setup,
}
return [bind(task, handlers[task.key]) for task in base]
def builtin_scenarios() -> list[Scenario]:
return [
Scenario(
name="baseline_workstation",
description="Socle poste Linux durci et outillé.",
task_keys=[
"system_update",
"automatic_updates",
"log_rotation",
"zsh_setup",
"utilities_setup",
"zram_setup",
"firewall_setup",
],
builtin=True,
),
Scenario(
name="security_audit",
description="Audit et vérifications de sécurité.",
task_keys=[
"system_update",
"lynis_audit",
"rootkit_check",
"firewall_setup",
"log_rotation",
],
builtin=True,
),
Scenario(
name="docker_host",
description="Socle serveur avec Docker et pare-feu.",
task_keys=[
"system_update",
"automatic_updates",
"firewall_setup",
"docker_setup",
"log_rotation",
],
builtin=True,
),
]

96
securecheck/config.py Normal file
View File

@@ -0,0 +1,96 @@
from __future__ import annotations
import os
import pwd
import tempfile
from dataclasses import dataclass
from pathlib import Path
@dataclass(frozen=True)
class AppPaths:
config_dir: Path
state_dir: Path
log_dir: Path
report_dir: Path
scenario_file: Path
app_log_file: Path
def _invoking_user() -> tuple[str, Path]:
sudo_user = os.environ.get("SUDO_USER")
if sudo_user:
user_info = pwd.getpwnam(sudo_user)
return sudo_user, Path(user_info.pw_dir)
user = os.environ.get("USER", "root")
home = Path.home()
return user, home
def build_paths() -> AppPaths:
_, user_home = _invoking_user()
config_home = Path(os.environ.get("XDG_CONFIG_HOME", user_home / ".config"))
state_home = Path(os.environ.get("XDG_STATE_HOME", user_home / ".local" / "state"))
config_dir = _select_writable_dir(
[
config_home / "securecheck",
Path.cwd() / ".securecheck-runtime" / "config",
Path(tempfile.gettempdir()) / "securecheck" / "config",
]
)
state_dir = _select_writable_dir(
[
state_home / "securecheck",
Path.cwd() / ".securecheck-runtime" / "state",
Path(tempfile.gettempdir()) / "securecheck" / "state",
]
)
if os.geteuid() == 0 and _is_path_writable(Path("/var/log")):
log_dir = Path("/var/log/securecheck")
else:
log_dir = _select_writable_dir(
[
state_dir / "logs",
Path.cwd() / ".securecheck-runtime" / "logs",
Path(tempfile.gettempdir()) / "securecheck" / "logs",
]
)
report_dir = log_dir / "reports"
scenario_file = config_dir / "scenarios.json"
app_log_file = log_dir / "securecheck.log"
return AppPaths(
config_dir=config_dir,
state_dir=state_dir,
log_dir=log_dir,
report_dir=report_dir,
scenario_file=scenario_file,
app_log_file=app_log_file,
)
def ensure_app_dirs(paths: AppPaths) -> None:
for directory in (paths.config_dir, paths.state_dir, paths.log_dir, paths.report_dir):
directory.mkdir(parents=True, exist_ok=True)
def _is_path_writable(path: Path) -> bool:
target = path if path.exists() else path.parent
return os.access(target, os.W_OK)
def _select_writable_dir(candidates: list[Path]) -> Path:
for candidate in candidates:
try:
candidate.mkdir(parents=True, exist_ok=True)
probe = candidate / ".write-test"
with probe.open("w", encoding="utf-8") as handle:
handle.write("ok")
probe.unlink()
return candidate
except OSError:
continue
raise OSError("Aucun emplacement inscriptible disponible pour SecureCheck")

368
securecheck/executor.py Normal file
View File

@@ -0,0 +1,368 @@
from __future__ import annotations
import json
import logging
import os
import shutil
import stat
import subprocess
import tempfile
import urllib.request
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from .config import AppPaths
from .models import TaskDefinition, TaskResult
from .system_info import SystemInfo
class SecureCheckError(RuntimeError):
"""Raised when a task cannot be completed."""
@dataclass
class CommandResult:
command: list[str]
returncode: int
stdout: str
stderr: str
@dataclass
class PackageOperation:
requested: list[str]
installed: list[str]
already_present: list[str]
@property
def changed(self) -> bool:
return bool(self.installed)
@dataclass
class ExecutionContext:
paths: AppPaths
system: SystemInfo
logger: logging.Logger
dry_run: bool = False
assume_yes: bool = True
def __post_init__(self) -> None:
self.runner = CommandRunner(self)
def info(self, message: str) -> None:
print(message)
self.logger.info(message)
def warning(self, message: str) -> None:
print(f"WARNING: {message}")
self.logger.warning(message)
def error(self, message: str) -> None:
print(f"ERROR: {message}")
self.logger.error(message)
def make_result(
self,
task: TaskDefinition,
*,
success: bool,
changed: bool,
started_at: datetime,
details: list[str] | None = None,
error: str | None = None,
) -> TaskResult:
return TaskResult(
key=task.key,
label=task.label,
success=success,
changed=changed,
started_at=started_at,
finished_at=datetime.now(),
details=details or [],
error=error,
)
class CommandRunner:
def __init__(self, context: ExecutionContext) -> None:
self.context = context
self._package_index_updated = False
def command_exists(self, command: str) -> bool:
return shutil.which(command) is not None
def run(
self,
command: list[str],
*,
requires_root: bool = False,
run_as_user: str | None = None,
check: bool = True,
capture_output: bool = True,
env: dict[str, str] | None = None,
input_text: str | None = None,
) -> CommandResult:
final_command = list(command)
if run_as_user and os.geteuid() == 0 and run_as_user != "root":
final_command = ["sudo", "-u", run_as_user] + final_command
elif requires_root and os.geteuid() != 0:
final_command = ["sudo"] + final_command
rendered = " ".join(final_command)
self.context.logger.info("Commande: %s", rendered)
if self.context.dry_run:
self.context.info(f"[dry-run] {rendered}")
return CommandResult(command=final_command, returncode=0, stdout="", stderr="")
completed = subprocess.run(
final_command,
text=True,
capture_output=capture_output,
env=env,
input=input_text,
check=False,
)
stdout = completed.stdout or ""
stderr = completed.stderr or ""
if stdout.strip():
self.context.logger.info("stdout:\n%s", stdout.rstrip())
if stderr.strip():
self.context.logger.warning("stderr:\n%s", stderr.rstrip())
if check and completed.returncode != 0:
raise SecureCheckError(f"Echec de la commande ({completed.returncode}): {rendered}")
return CommandResult(
command=final_command,
returncode=completed.returncode,
stdout=stdout,
stderr=stderr,
)
def update_package_index(self) -> None:
if self._package_index_updated:
return
manager = self.context.system.package_manager
if manager == "apt-get":
self.run(["apt-get", "update"], requires_root=True)
elif manager in {"dnf", "yum"}:
self.run([manager, "makecache"], requires_root=True)
elif manager == "pacman":
self.run(["pacman", "-Sy"], requires_root=True)
else:
raise SecureCheckError("Gestionnaire de paquets non supporté")
self._package_index_updated = True
def is_package_installed(self, package: str) -> bool:
manager = self.context.system.package_manager
if manager == "apt-get":
return subprocess.run(["dpkg", "-s", package], capture_output=True).returncode == 0
if manager in {"dnf", "yum"}:
return subprocess.run(["rpm", "-q", package], capture_output=True).returncode == 0
if manager == "pacman":
return subprocess.run(["pacman", "-Q", package], capture_output=True).returncode == 0
return False
def package_available(self, package: str) -> bool:
manager = self.context.system.package_manager
if manager == "apt-get":
self.update_package_index()
return subprocess.run(["apt-cache", "show", package], capture_output=True).returncode == 0
if manager in {"dnf", "yum"}:
return subprocess.run([manager, "info", package], capture_output=True).returncode == 0
if manager == "pacman":
return subprocess.run(["pacman", "-Si", package], capture_output=True).returncode == 0
return False
def ensure_packages(self, packages: list[str]) -> bool:
return self.ensure_packages_report(packages).changed
def ensure_packages_report(self, packages: list[str]) -> PackageOperation:
if not packages:
return PackageOperation(requested=[], installed=[], already_present=[])
manager = self.context.system.package_manager
self.update_package_index()
already_present = [package for package in packages if self.is_package_installed(package)]
missing = [package for package in packages if package not in already_present]
if not missing:
return PackageOperation(requested=packages, installed=[], already_present=already_present)
if manager == "apt-get":
command = ["apt-get", "install", "-y", *missing]
elif manager in {"dnf", "yum"}:
command = [manager, "install", "-y", *missing]
elif manager == "pacman":
command = ["pacman", "-S", "--noconfirm", *missing]
else:
raise SecureCheckError("Installation de paquets non supportée")
self.run(command, requires_root=True)
return PackageOperation(requested=packages, installed=missing, already_present=already_present)
def upgrade_system(self) -> None:
manager = self.context.system.package_manager
if manager == "apt-get":
self.run(["apt-get", "dist-upgrade", "-y"], requires_root=True)
self.run(["apt-get", "autoremove", "-y"], requires_root=True)
self.run(["apt-get", "autoclean"], requires_root=True)
elif manager in {"dnf", "yum"}:
self.run([manager, "upgrade", "-y", "--refresh"], requires_root=True)
elif manager == "pacman":
self.run(["pacman", "-Syu", "--noconfirm"], requires_root=True)
else:
raise SecureCheckError("Mise à jour système non supportée")
def ensure_directory(self, path: Path, mode: int = 0o755, *, requires_root: bool = False) -> None:
if self.context.dry_run:
self.context.info(f"[dry-run] mkdir -p {path}")
return
if requires_root and os.geteuid() != 0:
self.run(["install", "-d", "-m", f"{mode:o}", str(path)], requires_root=True)
return
path.mkdir(parents=True, exist_ok=True)
path.chmod(mode)
if requires_root and os.geteuid() == 0:
os.chown(path, 0, 0)
def write_text_file(
self,
path: Path,
content: str,
*,
mode: int = 0o644,
requires_root: bool = False,
owner_uid: int | None = None,
owner_gid: int | None = None,
) -> bool:
try:
current = path.read_text(encoding="utf-8") if path.exists() else None
except OSError:
current = None
if current == content:
return False
self.context.logger.info("Ecriture du fichier %s", path)
if self.context.dry_run:
self.context.info(f"[dry-run] write {path}")
return True
if requires_root and os.geteuid() != 0:
self._write_text_file_as_root(path, content, mode=mode, owner_uid=owner_uid, owner_gid=owner_gid)
return True
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
os.chmod(path, mode)
if requires_root and os.geteuid() == 0:
os.chown(path, 0, 0)
elif owner_uid is not None and owner_gid is not None and os.geteuid() == 0:
os.chown(path, owner_uid, owner_gid)
return True
def _write_text_file_as_root(
self,
path: Path,
content: str,
*,
mode: int,
owner_uid: int | None,
owner_gid: int | None,
) -> None:
self.ensure_directory(path.parent, requires_root=True)
tmp_dir = self.context.paths.state_dir
tmp_dir.mkdir(parents=True, exist_ok=True)
tmp_path: Path | None = None
try:
with tempfile.NamedTemporaryFile("w", encoding="utf-8", delete=False, dir=tmp_dir) as handle:
handle.write(content)
tmp_path = Path(handle.name)
self.run(["install", "-m", f"{mode:o}", str(tmp_path), str(path)], requires_root=True)
if owner_uid is not None and owner_gid is not None:
self.run(["chown", f"{owner_uid}:{owner_gid}", str(path)], requires_root=True)
finally:
if tmp_path and tmp_path.exists():
tmp_path.unlink(missing_ok=True)
def ensure_user_shell(self, shell_path: str) -> bool:
passwd_file = Path("/etc/passwd").read_text(encoding="utf-8")
for line in passwd_file.splitlines():
if line.startswith(f"{self.context.system.target_user}:"):
current_shell = line.rsplit(":", 1)[-1]
if current_shell == shell_path:
return False
break
self.run(["chsh", "-s", shell_path, self.context.system.target_user], requires_root=os.geteuid() == 0)
return True
def enable_service(self, service: str) -> None:
self.run(["systemctl", "enable", "--now", service], requires_root=True)
def restart_service(self, service: str) -> None:
self.run(["systemctl", "restart", service], requires_root=True)
def service_is_active(self, service: str) -> bool:
result = self.run(["systemctl", "is-active", service], requires_root=True, check=False)
return result.returncode == 0
def read_memory_mb(self) -> int:
for line in Path("/proc/meminfo").read_text(encoding="utf-8").splitlines():
if line.startswith("MemTotal:"):
parts = line.split()
return int(parts[1]) // 1024
return 1024
def write_json_file(self, path: Path, payload: dict, *, mode: int = 0o644, requires_root: bool = False) -> bool:
content = json.dumps(payload, indent=2) + "\n"
return self.write_text_file(path, content, mode=mode, requires_root=requires_root)
def ensure_executable(self, path: Path, *, requires_root: bool = False) -> None:
if self.context.dry_run:
return
current = stat.S_IMODE(path.stat().st_mode)
path.chmod(current | 0o111)
if requires_root and os.geteuid() == 0:
os.chown(path, 0, 0)
def download_text(self, url: str, *, timeout: int = 20) -> str:
self.context.logger.info("Téléchargement: %s", url)
if self.context.dry_run:
self.context.info(f"[dry-run] download {url}")
return ""
with urllib.request.urlopen(url, timeout=timeout) as response:
return response.read().decode("utf-8")
def execute_tasks(context: ExecutionContext, tasks: list[TaskDefinition]) -> list[TaskResult]:
results: list[TaskResult] = []
total = len(tasks)
for index, task in enumerate(tasks, start=1):
started_at = datetime.now()
context.info(f"[{index}/{total}] {task.label}")
try:
result = task.handler(context)
results.append(result)
status = "OK" if result.success else "ECHEC"
context.info(f" -> {status} ({result.duration_seconds:.1f}s)")
except Exception as exc: # noqa: BLE001
context.logger.exception("Task failed: %s", task.key)
results.append(
context.make_result(
task,
success=False,
changed=False,
started_at=started_at,
details=[],
error=str(exc),
)
)
context.error(f" -> ECHEC: {exc}")
return results

View File

@@ -0,0 +1,34 @@
from __future__ import annotations
import logging
from logging.handlers import RotatingFileHandler
from pathlib import Path
def setup_logging(log_file: Path) -> logging.Logger:
logger = logging.getLogger("securecheck")
if logger.handlers:
return logger
logger.setLevel(logging.INFO)
logger.propagate = False
formatter = logging.Formatter(
fmt="%(asctime)s | %(levelname)s | %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
file_handler = RotatingFileHandler(log_file, maxBytes=1_000_000, backupCount=5, encoding="utf-8")
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)
return logger
def attach_run_handler(logger: logging.Logger, run_log_file: Path) -> RotatingFileHandler:
formatter = logging.Formatter(
fmt="%(asctime)s | %(levelname)s | %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
handler = RotatingFileHandler(run_log_file, maxBytes=2_000_000, backupCount=2, encoding="utf-8")
handler.setFormatter(formatter)
logger.addHandler(handler)
return handler

46
securecheck/models.py Normal file
View File

@@ -0,0 +1,46 @@
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime
from typing import Callable, TYPE_CHECKING
if TYPE_CHECKING:
from .executor import ExecutionContext
TaskHandler = Callable[["ExecutionContext"], "TaskResult"]
@dataclass(frozen=True)
class TaskDefinition:
key: str
label: str
description: str
category: str
handler: TaskHandler
requires_root: bool = True
default_selected: bool = False
@dataclass
class TaskResult:
key: str
label: str
success: bool
changed: bool
started_at: datetime
finished_at: datetime
details: list[str] = field(default_factory=list)
error: str | None = None
@property
def duration_seconds(self) -> float:
return (self.finished_at - self.started_at).total_seconds()
@dataclass
class Scenario:
name: str
task_keys: list[str]
description: str = ""
builtin: bool = False

100
securecheck/status.py Normal file
View File

@@ -0,0 +1,100 @@
from __future__ import annotations
import shutil
import subprocess
from dataclasses import dataclass
from pathlib import Path
from .system_info import SystemInfo
@dataclass(frozen=True)
class StatusItem:
category: str
label: str
ok: bool
detail: str
def _command_exists(command: str) -> bool:
return shutil.which(command) is not None
def _run(command: list[str]) -> subprocess.CompletedProcess[str]:
return subprocess.run(command, text=True, capture_output=True, check=False)
def _service_active(service: str) -> bool:
if not _command_exists("systemctl"):
return False
return _run(["systemctl", "is-active", service]).returncode == 0
def _binary_status(category: str, label: str, command: str) -> StatusItem:
exists = _command_exists(command)
return StatusItem(category=category, label=label, ok=exists, detail="installé" if exists else "absent")
def collect_status(system: SystemInfo) -> list[StatusItem]:
maintenance: list[StatusItem] = []
security: list[StatusItem] = []
services: list[StatusItem] = []
performance: list[StatusItem] = []
poste: list[StatusItem] = []
p10k_path = system.target_home / ".p10k.zsh"
poste.append(StatusItem("Poste", "Config p10k", p10k_path.exists(), str(p10k_path if p10k_path.exists() else "absente")))
unattended_ok = False
unattended_detail = "non configuré"
if system.package_manager == "apt-get":
unattended_ok = _service_active("unattended-upgrades.service") and Path("/etc/apt/apt.conf.d/20auto-upgrades").exists()
unattended_detail = "service actif" if unattended_ok else "service inactif"
elif system.package_manager in {"dnf", "yum"}:
unattended_ok = _service_active("dnf-automatic.timer")
unattended_detail = "timer actif" if unattended_ok else "timer inactif"
maintenance.append(StatusItem("Maintenance", "MAJ auto", unattended_ok, unattended_detail))
logrotate_ok = Path("/etc/logrotate.d/securecheck").exists()
maintenance.append(StatusItem("Maintenance", "Rotation logs", logrotate_ok, "config présente" if logrotate_ok else "config absente"))
zram_ok = _service_active("securecheck-zram.service") or ("zram" in _run(["swapon", "--show"]).stdout if _command_exists("swapon") else False)
performance.append(StatusItem("Performance", "zram", zram_ok, "actif" if zram_ok else "inactif"))
if _command_exists("ufw"):
ufw_result = _run(["ufw", "status"])
firewall_ok = "Status: active" in ufw_result.stdout
firewall_detail = "ufw actif" if firewall_ok else "ufw inactif"
elif _command_exists("firewall-cmd"):
firewall_ok = _service_active("firewalld.service")
firewall_detail = "firewalld actif" if firewall_ok else "firewalld inactif"
else:
firewall_ok = False
firewall_detail = "pare-feu absent"
security.append(StatusItem("Sécurité", "Firewall", firewall_ok, firewall_detail))
security.append(_binary_status("Sécurité", "Lynis", "lynis"))
security.append(_binary_status("Sécurité", "rkhunter", "rkhunter"))
security.append(_binary_status("Sécurité", "chkrootkit", "chkrootkit"))
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(_binary_status("Services", "Docker", "docker"))
services.append(_binary_status("Services", "fail2ban", "fail2ban-client"))
poste.extend([
_binary_status("Poste", "zsh", "zsh"),
_binary_status("Poste", "git", "git"),
_binary_status("Poste", "curl", "curl"),
_binary_status("Poste", "ncdu", "ncdu"),
_binary_status("Poste", "needrestart", "needrestart"),
_binary_status("Poste", "htop", "htop"),
_binary_status("Poste", "nmon", "nmon"),
_binary_status("Poste", "duf", "duf"),
_binary_status("Poste", "net-tools", "ifconfig"),
])
ordered = maintenance + security + performance + services + poste
return ordered

74
securecheck/storage.py Normal file
View File

@@ -0,0 +1,74 @@
from __future__ import annotations
import json
from pathlib import Path
from .models import Scenario
class ScenarioStore:
def __init__(self, scenario_file: Path, builtin_scenarios: list[Scenario]) -> None:
self._scenario_file = scenario_file
self._builtin = {scenario.name: scenario for scenario in builtin_scenarios}
def _load_user_scenarios(self) -> dict[str, Scenario]:
if not self._scenario_file.exists():
return {}
raw = json.loads(self._scenario_file.read_text(encoding="utf-8"))
scenarios: dict[str, Scenario] = {}
for item in raw.get("scenarios", []):
scenario = Scenario(
name=item["name"],
description=item.get("description", ""),
task_keys=list(item.get("task_keys", [])),
builtin=False,
)
scenarios[scenario.name] = scenario
return scenarios
def list_all(self) -> list[Scenario]:
merged = dict(self._builtin)
merged.update(self._load_user_scenarios())
return sorted(merged.values(), key=lambda scenario: (scenario.builtin is False, scenario.name.lower()))
def get(self, name: str) -> Scenario | None:
return {scenario.name: scenario for scenario in self.list_all()}.get(name)
def save(self, scenario: Scenario) -> None:
scenarios = self._load_user_scenarios()
scenarios[scenario.name] = Scenario(
name=scenario.name,
description=scenario.description,
task_keys=scenario.task_keys,
builtin=False,
)
payload = {
"scenarios": [
{
"name": item.name,
"description": item.description,
"task_keys": item.task_keys,
}
for item in sorted(scenarios.values(), key=lambda s: s.name.lower())
]
}
self._scenario_file.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
def delete(self, name: str) -> bool:
scenarios = self._load_user_scenarios()
if name not in scenarios:
return False
del scenarios[name]
payload = {
"scenarios": [
{
"name": item.name,
"description": item.description,
"task_keys": item.task_keys,
}
for item in sorted(scenarios.values(), key=lambda s: s.name.lower())
]
}
self._scenario_file.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
return True

View File

@@ -0,0 +1,60 @@
from __future__ import annotations
import os
import pwd
import shutil
from dataclasses import dataclass
from pathlib import Path
@dataclass(frozen=True)
class SystemInfo:
distro_id: str
pretty_name: str
package_manager: str
target_user: str
target_home: Path
target_uid: int
target_gid: int
def _read_os_release() -> dict[str, str]:
values: dict[str, str] = {}
release_file = Path("/etc/os-release")
if not release_file.exists():
return values
for line in release_file.read_text(encoding="utf-8").splitlines():
if "=" not in line or line.startswith("#"):
continue
key, value = line.split("=", 1)
values[key] = value.strip().strip('"')
return values
def detect_package_manager() -> str:
for candidate in ("apt-get", "dnf", "yum", "pacman"):
if shutil.which(candidate):
return candidate
return "unknown"
def resolve_target_user() -> tuple[str, Path, int, int]:
user_name = os.environ.get("SUDO_USER") or os.environ.get("USER") or "root"
user_info = pwd.getpwnam(user_name)
return user_name, Path(user_info.pw_dir), user_info.pw_uid, user_info.pw_gid
def detect_system() -> SystemInfo:
values = _read_os_release()
package_manager = detect_package_manager()
user_name, user_home, uid, gid = resolve_target_user()
return SystemInfo(
distro_id=values.get("ID", "unknown"),
pretty_name=values.get("PRETTY_NAME", "Linux"),
package_manager=package_manager,
target_user=user_name,
target_home=user_home,
target_uid=uid,
target_gid=gid,
)

449
securecheck/tasks.py Normal file
View File

@@ -0,0 +1,449 @@
from __future__ import annotations
import json
from datetime import datetime
from pathlib import Path
from .assets import asset_text
from .executor import ExecutionContext, SecureCheckError
from .models import TaskDefinition, TaskResult
P10K_REMOTE_URL = "https://git.h3campus.fr/Johnny/Install_zsh/raw/branch/main/.p10k.zsh"
P10K_THEME_GIT_URL = "https://github.com/romkatv/powerlevel10k.git"
def _result(
context: ExecutionContext,
task: TaskDefinition,
started_at: datetime,
*,
changed: bool,
details: list[str] | None = None,
) -> TaskResult:
return context.make_result(task, success=True, changed=changed, started_at=started_at, details=details or [])
def _write_report(context: ExecutionContext, name: str, content: str) -> Path:
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
report_path = context.paths.report_dir / f"{timestamp}-{name}.log"
context.runner.write_text_file(report_path, content, mode=0o640, requires_root=False)
return report_path
def _append_package_details(context: ExecutionContext, details: list[str], report) -> bool:
changed = report.changed
added_label = "Seraient ajoutés" if context.dry_run else "Ajoutés"
if report.already_present:
details.append(f"Déjà présents: {', '.join(report.already_present)}")
if report.installed:
details.append(f"{added_label}: {', '.join(report.installed)}")
return changed
def system_update(context: ExecutionContext, task: TaskDefinition) -> TaskResult:
started_at = datetime.now()
context.runner.update_package_index()
context.runner.upgrade_system()
details = ["Index des paquets rafraîchi", "Mises à jour système appliquées"]
return _result(context, task, started_at, changed=True, details=details)
def automatic_updates(context: ExecutionContext, task: TaskDefinition) -> TaskResult:
started_at = datetime.now()
manager = context.system.package_manager
changed = False
details: list[str] = []
if manager == "apt-get":
pkg_report = context.runner.ensure_packages_report(["unattended-upgrades", "apt-listchanges"])
changed |= _append_package_details(context, details, pkg_report)
content_20 = """APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Download-Upgradeable-Packages "1";
APT::Periodic::AutocleanInterval "7";
APT::Periodic::Unattended-Upgrade "1";
"""
content_52 = """Unattended-Upgrade::Automatic-Reboot "false";
Unattended-Upgrade::Automatic-Reboot-Time "03:30";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Remove-New-Unused-Dependencies "true";
"""
changed |= context.runner.write_text_file(Path("/etc/apt/apt.conf.d/20auto-upgrades"), content_20, requires_root=True)
changed |= context.runner.write_text_file(
Path("/etc/apt/apt.conf.d/52securecheck-unattended-upgrades"),
content_52,
requires_root=True,
)
context.runner.enable_service("unattended-upgrades.service")
details.append("Mises à jour automatiques APT configurées")
elif manager in {"dnf", "yum"}:
pkg_report = context.runner.ensure_packages_report(["dnf-automatic"])
changed |= _append_package_details(context, details, pkg_report)
changed |= context.runner.write_text_file(
Path("/etc/dnf/automatic.conf"),
"""[commands]
apply_updates = yes
upgrade_type = default
[emitters]
emit_via = stdio
system_name = securecheck
""",
requires_root=True,
)
context.runner.enable_service("dnf-automatic.timer")
details.append("Mises à jour automatiques DNF configurées")
else:
raise SecureCheckError("Les mises à jour automatiques ne sont pas prises en charge sur ce système")
return _result(context, task, started_at, changed=changed, details=details)
def lynis_audit(context: ExecutionContext, task: TaskDefinition) -> TaskResult:
started_at = datetime.now()
details: list[str] = []
pkg_report = context.runner.ensure_packages_report(["lynis"])
changed = _append_package_details(context, details, pkg_report)
result = context.runner.run(["lynis", "audit", "system", "--quick"], requires_root=True, check=False)
report_body = "\n".join(
[
"=== SecureCheck / Lynis ===",
f"Return code: {result.returncode}",
"",
result.stdout,
result.stderr,
]
).strip() + "\n"
report_path = _write_report(context, "lynis", report_body)
details.append(f"Rapport Lynis: {report_path}")
success = result.returncode == 0
return context.make_result(task, success=success, changed=changed, started_at=started_at, details=details, error=None if success else "Lynis a remonté une erreur")
def rootkit_check(context: ExecutionContext, task: TaskDefinition) -> TaskResult:
started_at = datetime.now()
details: list[str] = []
pkg_report = context.runner.ensure_packages_report(["rkhunter", "chkrootkit"])
changed = _append_package_details(context, details, pkg_report)
update_result = context.runner.run(["rkhunter", "--update"], requires_root=True, check=False)
details.append(f"rkhunter update rc={update_result.returncode}")
propupd_result = context.runner.run(["rkhunter", "--propupd"], requires_root=True, check=False)
details.append(f"rkhunter propupd rc={propupd_result.returncode}")
rkhunter_result = context.runner.run(
["rkhunter", "--check", "--skip-keypress", "--report-warnings-only"],
requires_root=True,
check=False,
)
chkrootkit_result = context.runner.run(["chkrootkit", "-q"], requires_root=True, check=False)
report_payload = {
"rkhunter_check_returncode": rkhunter_result.returncode,
"chkrootkit_returncode": chkrootkit_result.returncode,
"rkhunter_stdout": rkhunter_result.stdout,
"rkhunter_stderr": rkhunter_result.stderr,
"chkrootkit_stdout": chkrootkit_result.stdout,
"chkrootkit_stderr": chkrootkit_result.stderr,
}
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")
def log_rotation(context: ExecutionContext, task: TaskDefinition) -> TaskResult:
started_at = datetime.now()
details: list[str] = []
pkg_report = context.runner.ensure_packages_report(["logrotate"])
changed = _append_package_details(context, details, pkg_report)
log_target = "/var/log/securecheck/*.log"
report_target = "/var/log/securecheck/reports/*"
content = f"""{log_target} {report_target} {{
rotate 7
daily
missingok
notifempty
compress
delaycompress
copytruncate
create 0640 root adm
}}
"""
changed |= context.runner.write_text_file(Path("/etc/logrotate.d/securecheck"), content, requires_root=True)
details.append("Rotation des logs SecureCheck configurée")
return _result(context, task, started_at, changed=changed, details=details)
def zsh_setup(context: ExecutionContext, task: TaskDefinition) -> TaskResult:
started_at = datetime.now()
manager = context.system.package_manager
details: list[str] = []
packages = ["zsh", "git", "curl"]
if manager == "apt-get":
packages += ["zsh-autosuggestions", "zsh-syntax-highlighting"]
for optional in ("zsh-theme-powerlevel10k", "fonts-powerline"):
if context.runner.package_available(optional):
packages.append(optional)
elif manager == "pacman":
for optional in ("zsh-autosuggestions", "zsh-syntax-highlighting", "zsh-theme-powerlevel10k"):
if context.runner.package_available(optional):
packages.append(optional)
pkg_report = context.runner.ensure_packages_report(packages)
changed = _append_package_details(context, details, pkg_report)
try:
p10k_content = context.runner.download_text(P10K_REMOTE_URL)
p10k_source = f"source distante: {P10K_REMOTE_URL}"
except Exception: # noqa: BLE001
p10k_content = asset_text("p10k.zsh")
p10k_source = "source locale embarquée: assets/p10k.zsh"
if not p10k_content:
p10k_content = asset_text("p10k.zsh")
p10k_source = "source locale embarquée: assets/p10k.zsh"
zshrc_path = context.system.target_home / ".zshrc"
p10k_path = context.system.target_home / ".p10k.zsh"
theme_repo_path = context.system.target_home / ".powerlevel10k"
theme_system_paths = [
Path("/usr/share/powerlevel10k/powerlevel10k.zsh-theme"),
Path("/usr/share/zsh-theme-powerlevel10k/powerlevel10k.zsh-theme"),
]
if not any(path.exists() for path in theme_system_paths):
if not theme_repo_path.exists():
context.runner.run(
["git", "clone", "--depth=1", P10K_THEME_GIT_URL, str(theme_repo_path)],
run_as_user=context.system.target_user,
)
changed = True
details.append(f"Theme powerlevel10k cloné dans {theme_repo_path}")
else:
details.append(f"Theme powerlevel10k déjà présent dans {theme_repo_path}")
zshrc_content = """# Fichier généré par SecureCheck
if [[ -r "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" ]]; then
source "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh"
fi
export HISTFILE="$HOME/.zsh_history"
export HISTSIZE=10000
export SAVEHIST=10000
setopt appendhistory
setopt histignoredups
setopt sharehistory
setopt autocd
autoload -Uz compinit
compinit
if [ -f /usr/share/powerlevel10k/powerlevel10k.zsh-theme ]; then
source /usr/share/powerlevel10k/powerlevel10k.zsh-theme
elif [ -f /usr/share/zsh-theme-powerlevel10k/powerlevel10k.zsh-theme ]; then
source /usr/share/zsh-theme-powerlevel10k/powerlevel10k.zsh-theme
elif [ -f "$HOME/.powerlevel10k/powerlevel10k.zsh-theme" ]; then
source "$HOME/.powerlevel10k/powerlevel10k.zsh-theme"
fi
if [ -f /usr/share/zsh-autosuggestions/zsh-autosuggestions.zsh ]; then
source /usr/share/zsh-autosuggestions/zsh-autosuggestions.zsh
fi
if [ -f /usr/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh ]; then
source /usr/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh
fi
bindkey '^[[A' history-search-backward
bindkey '^[[B' history-search-forward
alias ll='ls -alF'
alias la='ls -A'
alias l='ls -CF'
alias update-system='sudo apt-get update && sudo apt-get dist-upgrade -y'
[[ -f "$HOME/.p10k.zsh" ]] && source "$HOME/.p10k.zsh"
"""
changed |= context.runner.write_text_file(
p10k_path,
p10k_content,
mode=0o644,
owner_uid=context.system.target_uid,
owner_gid=context.system.target_gid,
)
changed |= context.runner.write_text_file(
zshrc_path,
zshrc_content,
mode=0o644,
owner_uid=context.system.target_uid,
owner_gid=context.system.target_gid,
)
zsh_path = "/usr/bin/zsh" if Path("/usr/bin/zsh").exists() else "/bin/zsh"
if Path(zsh_path).exists():
changed |= context.runner.ensure_user_shell(zsh_path)
details.append(f"Configuration zsh appliquée pour {context.system.target_user}")
details.append(f"Fichier p10k copié vers {p10k_path}")
details.append(p10k_source)
return _result(context, task, started_at, changed=changed, details=details)
def utilities_setup(context: ExecutionContext, task: TaskDefinition) -> TaskResult:
started_at = datetime.now()
manager = context.system.package_manager
if manager == "apt-get":
packages = [
"ncdu",
"needrestart",
"git",
"curl",
"fail2ban",
"htop",
"nmon",
"duf",
"net-tools",
"tmux",
"tree",
"vim",
"ca-certificates",
]
elif manager in {"dnf", "yum"}:
packages = ["ncdu", "git", "curl", "fail2ban", "htop", "nmon", "duf", "net-tools", "tmux", "tree", "vim-enhanced"]
else:
packages = ["ncdu", "git", "curl", "htop", "nmon", "duf", "net-tools", "tmux", "tree", "vim"]
details: list[str] = []
pkg_report = context.runner.ensure_packages_report(packages)
changed = _append_package_details(context, details, pkg_report)
if context.runner.command_exists("systemctl") and context.runner.command_exists("fail2ban-client"):
context.runner.enable_service("fail2ban.service")
details.append("Utilitaires système et sécurité installés / vérifiés")
return _result(context, task, started_at, changed=changed, details=details)
def zram_setup(context: ExecutionContext, task: TaskDefinition) -> TaskResult:
started_at = datetime.now()
changed = False
details: list[str] = []
ram_mb = context.runner.read_memory_mb()
zram_mb = max(512, ram_mb // 2)
defaults_content = f"""# Géré par SecureCheck
ALGO=zstd
PERCENT=50
PRIORITY=100
ZRAM_SIZE={zram_mb}
"""
start_script = """#!/bin/sh
set -eu
modprobe zram || true
sleep 1
echo zstd > /sys/block/zram0/comp_algorithm
echo ${ZRAM_SIZE}M > /sys/block/zram0/disksize
mkswap /dev/zram0
swapon -p 100 /dev/zram0
"""
stop_script = """#!/bin/sh
swapoff /dev/zram0 2>/dev/null || true
echo 1 > /sys/block/zram0/reset 2>/dev/null || true
rmmod zram 2>/dev/null || true
"""
service_content = """[Unit]
Description=SecureCheck zram swap
After=local-fs.target
Before=swap.target
[Service]
Type=oneshot
RemainAfterExit=yes
EnvironmentFile=/etc/default/securecheck-zram
ExecStart=/usr/local/bin/securecheck-zram-start
ExecStop=/usr/local/bin/securecheck-zram-stop
[Install]
WantedBy=multi-user.target
"""
changed |= context.runner.write_text_file(Path("/etc/default/securecheck-zram"), defaults_content, requires_root=True)
changed |= context.runner.write_text_file(Path("/usr/local/bin/securecheck-zram-start"), start_script, mode=0o755, requires_root=True)
changed |= context.runner.write_text_file(Path("/usr/local/bin/securecheck-zram-stop"), stop_script, mode=0o755, requires_root=True)
changed |= context.runner.write_text_file(Path("/etc/systemd/system/securecheck-zram.service"), service_content, requires_root=True)
context.runner.run(["systemctl", "daemon-reload"], requires_root=True)
context.runner.enable_service("securecheck-zram.service")
details.append(f"zram configuré à {zram_mb} Mo")
return _result(context, task, started_at, changed=changed, details=details)
def firewall_setup(context: ExecutionContext, task: TaskDefinition) -> TaskResult:
started_at = datetime.now()
manager = context.system.package_manager
changed = False
details: list[str] = []
if manager == "apt-get":
pkg_report = context.runner.ensure_packages_report(["ufw"])
changed |= _append_package_details(context, details, pkg_report)
context.runner.run(["ufw", "default", "deny", "incoming"], requires_root=True)
context.runner.run(["ufw", "default", "allow", "outgoing"], requires_root=True)
ssh_rule = context.runner.run(["ufw", "status"], requires_root=True, check=False)
if "22/tcp" not in ssh_rule.stdout and "OpenSSH" not in ssh_rule.stdout:
context.runner.run(["ufw", "allow", "22/tcp"], requires_root=True)
changed = True
context.runner.run(["ufw", "--force", "enable"], requires_root=True)
details.append("Pare-feu UFW activé")
elif manager in {"dnf", "yum"}:
pkg_report = context.runner.ensure_packages_report(["firewalld"])
changed |= _append_package_details(context, details, pkg_report)
context.runner.enable_service("firewalld.service")
context.runner.run(["firewall-cmd", "--permanent", "--add-service=ssh"], requires_root=True)
context.runner.run(["firewall-cmd", "--reload"], requires_root=True)
details.append("Pare-feu firewalld activé")
else:
raise SecureCheckError("Pare-feu automatique non pris en charge sur ce système")
return _result(context, task, started_at, changed=changed, details=details)
def docker_setup(context: ExecutionContext, task: TaskDefinition) -> TaskResult:
started_at = datetime.now()
manager = context.system.package_manager
changed = False
details: list[str] = []
if manager == "apt-get":
pkg_report = context.runner.ensure_packages_report(["docker.io", "docker-compose-v2"])
changed |= _append_package_details(context, details, pkg_report)
elif manager in {"dnf", "yum", "pacman"}:
pkg_report = context.runner.ensure_packages_report(["docker"])
changed |= _append_package_details(context, details, pkg_report)
else:
raise SecureCheckError("Docker n'est pas pris en charge sur ce système")
daemon_payload = {
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3",
},
}
changed |= context.runner.write_json_file(Path("/etc/docker/daemon.json"), daemon_payload, requires_root=True)
context.runner.enable_service("docker.service")
context.runner.run(["usermod", "-aG", "docker", context.system.target_user], requires_root=True, check=False)
version_result = context.runner.run(["docker", "--version"], requires_root=False, check=False)
details.append(version_result.stdout.strip() or "Docker installé / vérifié")
return _result(context, task, started_at, changed=changed, details=details)
def bind(task: TaskDefinition, func) -> TaskDefinition:
return TaskDefinition(
key=task.key,
label=task.label,
description=task.description,
category=task.category,
requires_root=task.requires_root,
default_selected=task.default_selected,
handler=lambda context, _task=task, _func=func: _func(context, _task),
)