From 751dc8892c44fd7d800a51eb570861f58845fa3c Mon Sep 17 00:00:00 2001 From: Johnny Date: Sun, 5 Apr 2026 18:56:26 +0200 Subject: [PATCH] Initial commit --- .codex | 0 .../state/logs/run-20260405-161309.log | 46 + .../state/logs/run-20260405-161328.log | 46 + .../state/logs/run-20260405-182445.log | 44 + .../state/logs/run-20260405-182455.log | 18 + .../state/logs/run-20260405-182536.log | 14 + .../state/logs/run-20260405-183704.log | 14 + .../state/logs/run-20260405-183724.log | 14 + .../state/logs/securecheck.log | 196 ++ README.md | 21 + build_executable.sh | 18 + pyproject.toml | 27 + securecheck/__init__.py | 5 + securecheck/__main__.py | 137 ++ .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 246 bytes .../__pycache__/__main__.cpython-313.pyc | Bin 0 -> 8430 bytes securecheck/__pycache__/app.cpython-313.pyc | Bin 0 -> 31707 bytes .../__pycache__/assets.cpython-313.pyc | Bin 0 -> 1029 bytes .../__pycache__/catalog.cpython-313.pyc | Bin 0 -> 4081 bytes .../__pycache__/config.cpython-313.pyc | Bin 0 -> 4627 bytes .../__pycache__/executor.cpython-313.pyc | Bin 0 -> 20989 bytes .../__pycache__/logging_utils.cpython-313.pyc | Bin 0 -> 1586 bytes .../__pycache__/models.cpython-313.pyc | Bin 0 -> 2166 bytes .../__pycache__/status.cpython-313.pyc | Bin 0 -> 5882 bytes .../__pycache__/storage.cpython-313.pyc | Bin 0 -> 4767 bytes .../__pycache__/system_info.cpython-313.pyc | Bin 0 -> 2991 bytes securecheck/__pycache__/tasks.cpython-313.pyc | Bin 0 -> 22664 bytes securecheck/app.py | 401 ++++ securecheck/assets.py | 15 + securecheck/assets/banner.txt | 6 + securecheck/assets/p10k.zsh | 1840 +++++++++++++++++ securecheck/assets/securecheck-icon.ico | Bin 0 -> 17897 bytes securecheck/assets/securecheck-icon.png | Bin 0 -> 17894 bytes securecheck/assets/securecheck-icon.svg | 31 + securecheck/catalog.py | 158 ++ securecheck/config.py | 96 + securecheck/executor.py | 368 ++++ securecheck/logging_utils.py | 34 + securecheck/models.py | 46 + securecheck/status.py | 100 + securecheck/storage.py | 74 + securecheck/system_info.py | 60 + securecheck/tasks.py | 449 ++++ 43 files changed, 4278 insertions(+) create mode 100644 .codex create mode 100644 .securecheck-runtime/state/logs/run-20260405-161309.log create mode 100644 .securecheck-runtime/state/logs/run-20260405-161328.log create mode 100644 .securecheck-runtime/state/logs/run-20260405-182445.log create mode 100644 .securecheck-runtime/state/logs/run-20260405-182455.log create mode 100644 .securecheck-runtime/state/logs/run-20260405-182536.log create mode 100644 .securecheck-runtime/state/logs/run-20260405-183704.log create mode 100644 .securecheck-runtime/state/logs/run-20260405-183724.log create mode 100644 .securecheck-runtime/state/logs/securecheck.log create mode 100755 build_executable.sh create mode 100644 pyproject.toml create mode 100644 securecheck/__init__.py create mode 100644 securecheck/__main__.py create mode 100644 securecheck/__pycache__/__init__.cpython-313.pyc create mode 100644 securecheck/__pycache__/__main__.cpython-313.pyc create mode 100644 securecheck/__pycache__/app.cpython-313.pyc create mode 100644 securecheck/__pycache__/assets.cpython-313.pyc create mode 100644 securecheck/__pycache__/catalog.cpython-313.pyc create mode 100644 securecheck/__pycache__/config.cpython-313.pyc create mode 100644 securecheck/__pycache__/executor.cpython-313.pyc create mode 100644 securecheck/__pycache__/logging_utils.cpython-313.pyc create mode 100644 securecheck/__pycache__/models.cpython-313.pyc create mode 100644 securecheck/__pycache__/status.cpython-313.pyc create mode 100644 securecheck/__pycache__/storage.cpython-313.pyc create mode 100644 securecheck/__pycache__/system_info.cpython-313.pyc create mode 100644 securecheck/__pycache__/tasks.cpython-313.pyc create mode 100644 securecheck/app.py create mode 100644 securecheck/assets.py create mode 100644 securecheck/assets/banner.txt create mode 100644 securecheck/assets/p10k.zsh create mode 100644 securecheck/assets/securecheck-icon.ico create mode 100644 securecheck/assets/securecheck-icon.png create mode 100644 securecheck/assets/securecheck-icon.svg create mode 100644 securecheck/catalog.py create mode 100644 securecheck/config.py create mode 100644 securecheck/executor.py create mode 100644 securecheck/logging_utils.py create mode 100644 securecheck/models.py create mode 100644 securecheck/status.py create mode 100644 securecheck/storage.py create mode 100644 securecheck/system_info.py create mode 100644 securecheck/tasks.py diff --git a/.codex b/.codex new file mode 100644 index 0000000..e69de29 diff --git a/.securecheck-runtime/state/logs/run-20260405-161309.log b/.securecheck-runtime/state/logs/run-20260405-161309.log new file mode 100644 index 0000000..8e6d508 --- /dev/null +++ b/.securecheck-runtime/state/logs/run-20260405-161309.log @@ -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) diff --git a/.securecheck-runtime/state/logs/run-20260405-161328.log b/.securecheck-runtime/state/logs/run-20260405-161328.log new file mode 100644 index 0000000..8c3a7a7 --- /dev/null +++ b/.securecheck-runtime/state/logs/run-20260405-161328.log @@ -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) diff --git a/.securecheck-runtime/state/logs/run-20260405-182445.log b/.securecheck-runtime/state/logs/run-20260405-182445.log new file mode 100644 index 0000000..84f8278 --- /dev/null +++ b/.securecheck-runtime/state/logs/run-20260405-182445.log @@ -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) diff --git a/.securecheck-runtime/state/logs/run-20260405-182455.log b/.securecheck-runtime/state/logs/run-20260405-182455.log new file mode 100644 index 0000000..e5754df --- /dev/null +++ b/.securecheck-runtime/state/logs/run-20260405-182455.log @@ -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) diff --git a/.securecheck-runtime/state/logs/run-20260405-182536.log b/.securecheck-runtime/state/logs/run-20260405-182536.log new file mode 100644 index 0000000..351e24b --- /dev/null +++ b/.securecheck-runtime/state/logs/run-20260405-182536.log @@ -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) diff --git a/.securecheck-runtime/state/logs/run-20260405-183704.log b/.securecheck-runtime/state/logs/run-20260405-183704.log new file mode 100644 index 0000000..d1fce70 --- /dev/null +++ b/.securecheck-runtime/state/logs/run-20260405-183704.log @@ -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) diff --git a/.securecheck-runtime/state/logs/run-20260405-183724.log b/.securecheck-runtime/state/logs/run-20260405-183724.log new file mode 100644 index 0000000..5d1b8f7 --- /dev/null +++ b/.securecheck-runtime/state/logs/run-20260405-183724.log @@ -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) diff --git a/.securecheck-runtime/state/logs/securecheck.log b/.securecheck-runtime/state/logs/securecheck.log new file mode 100644 index 0000000..147a056 --- /dev/null +++ b/.securecheck-runtime/state/logs/securecheck.log @@ -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) diff --git a/README.md b/README.md index a591fd9..cfc6363 100644 --- a/README.md +++ b/README.md @@ -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` diff --git a/build_executable.sh b/build_executable.sh new file mode 100755 index 0000000..b315dce --- /dev/null +++ b/build_executable.sh @@ -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" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..11363c1 --- /dev/null +++ b/pyproject.toml @@ -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/*"] diff --git a/securecheck/__init__.py b/securecheck/__init__.py new file mode 100644 index 0000000..27b6746 --- /dev/null +++ b/securecheck/__init__.py @@ -0,0 +1,5 @@ +"""SecureCheck package.""" + +__all__ = ["__version__"] + +__version__ = "0.1.0" diff --git a/securecheck/__main__.py b/securecheck/__main__.py new file mode 100644 index 0000000..6381c79 --- /dev/null +++ b/securecheck/__main__.py @@ -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()) diff --git a/securecheck/__pycache__/__init__.cpython-313.pyc b/securecheck/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bda130249d96529a6bd4420985d3c2bfb18d5950 GIT binary patch literal 246 zcmey&%ge<81hu)BGJS#cV-N=h7@>^MLO{k;hG2#whG0g0#v;aGrXr?vW=)nVk>J$i z(xO!7jMU_8g@VN7?8Nj`y<6Pz@nxw+#hLke@$prx26~2i27a2%x7g$3Q}UDJ<3WtX zoSgXhA|{}@D;Yk6thr^SpOK%Ns$WuCkzNVZtzVp6lvz+xtX~YaAsJ)?3OhbNGcU6w zK3=b&@)n0pZhlH>PO4oI2hb>xvx`N5#0O?ZM#h^AQV+N#I`kXZAMlAbau=}yB>=G{ BL}~y4 literal 0 HcmV?d00001 diff --git a/securecheck/__pycache__/__main__.cpython-313.pyc b/securecheck/__pycache__/__main__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bafd762c0ae6649799e9621c6acf3cd0ba779f3b GIT binary patch literal 8430 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 ZtmW1ISNeZ;;nu}Z*z!k4!q%uv{4ZvcKE?n5 literal 0 HcmV?d00001 diff --git a/securecheck/__pycache__/app.cpython-313.pyc b/securecheck/__pycache__/app.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4246c901db395d5d59a20ea82a90abb8bc4ea958 GIT binary patch literal 31707 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 literal 0 HcmV?d00001 diff --git a/securecheck/__pycache__/config.cpython-313.pyc b/securecheck/__pycache__/config.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..187177ed9f31119a2502548333f0a9cf8f6eb1b5 GIT binary patch literal 4627 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 literal 0 HcmV?d00001 diff --git a/securecheck/__pycache__/executor.cpython-313.pyc b/securecheck/__pycache__/executor.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5718428e71657748c92adec80def0ae12e902661 GIT binary patch literal 20989 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 Pk^kG*ZH)#^`ThR@Nvg~M;WpgEs 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 ZLkc(e^q9!ypY{pdyWBrB87{KYzX8jTe!2hv literal 0 HcmV?d00001 diff --git a/securecheck/__pycache__/tasks.cpython-313.pyc b/securecheck/__pycache__/tasks.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c8a8c14fafede6a71bf2bacf424cade2c348d8cb GIT binary patch literal 22664 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 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) diff --git a/securecheck/assets.py b/securecheck/assets.py new file mode 100644 index 0000000..589c8ab --- /dev/null +++ b/securecheck/assets.py @@ -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") diff --git a/securecheck/assets/banner.txt b/securecheck/assets/banner.txt new file mode 100644 index 0000000..4dbfafe --- /dev/null +++ b/securecheck/assets/banner.txt @@ -0,0 +1,6 @@ + _____ _____ _ _ + / ____| / ____| | | | + | (___ ___ ___ _ _ _ __ ___| | | |__ ___ ___| | __ + \___ \ / _ \/ __| | | | '__/ _ \ | | '_ \ / _ \/ __| |/ / + ____) | __/ (__| |_| | | | __/ |____| | | | __/ (__| < + |_____/ \___|\___|\__,_|_| \___|\_____|_| |_|\___|\___|_|\_\ diff --git a/securecheck/assets/p10k.zsh b/securecheck/assets/p10k.zsh new file mode 100644 index 0000000..0dfdeaf --- /dev/null +++ b/securecheck/assets/p10k.zsh @@ -0,0 +1,1840 @@ +# Generated by Powerlevel10k configuration wizard on 2026-01-29 at 12:30 UTC. +# Based on romkatv/powerlevel10k/config/p10k-rainbow.zsh, checksum 57633. +# Wizard options: awesome-fontconfig + powerline, large icons, rainbow, unicode, +# 24h time, vertical separators, sharp heads, sharp tails, 2 lines, solid, full frame, +# dark-ornaments, sparse, many icons, concise, transient_prompt, instant_prompt=verbose. +# Type `p10k configure` to generate another config. +# +# Config for Powerlevel10k with powerline prompt style with colorful background. +# Type `p10k configure` to generate your own config based on it. +# +# Tip: Looking for a nice color? Here's a one-liner to print colormap. +# +# for i in {0..255}; do print -Pn "%K{$i} %k%F{$i}${(l:3::0:)i}%f " ${${(M)$((i%6)):#3}:+$'\n'}; done + +# Temporarily change options. +'builtin' 'local' '-a' 'p10k_config_opts' +[[ ! -o 'aliases' ]] || p10k_config_opts+=('aliases') +[[ ! -o 'sh_glob' ]] || p10k_config_opts+=('sh_glob') +[[ ! -o 'no_brace_expand' ]] || p10k_config_opts+=('no_brace_expand') +'builtin' 'setopt' 'no_aliases' 'no_sh_glob' 'brace_expand' + +() { + emulate -L zsh -o extended_glob + + # Unset all configuration options. This allows you to apply configuration changes without + # restarting zsh. Edit ~/.p10k.zsh and type `source ~/.p10k.zsh`. + unset -m '(POWERLEVEL9K_*|DEFAULT_USER)~POWERLEVEL9K_GITSTATUS_DIR' + + # Zsh >= 5.1 is required. + [[ $ZSH_VERSION == (5.<1->*|<6->.*) ]] || return + + # The list of segments shown on the left. Fill it with the most important segments. + typeset -g POWERLEVEL9K_LEFT_PROMPT_ELEMENTS=( + # =========================[ Line #1 ]========================= + os_icon # os identifier + dir # current directory + vcs # git status + # =========================[ Line #2 ]========================= + newline # \n + # prompt_char # prompt symbol + ) + + # The list of segments shown on the right. Fill it with less important segments. + # Right prompt on the last prompt line (where you are typing your commands) gets + # automatically hidden when the input line reaches it. Right prompt above the + # last prompt line gets hidden if it would overlap with left prompt. + typeset -g POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS=( + # =========================[ Line #1 ]========================= + status # exit code of the last command + command_execution_time # duration of the last command + background_jobs # presence of background jobs + direnv # direnv status (https://direnv.net/) + asdf # asdf version manager (https://github.com/asdf-vm/asdf) + virtualenv # python virtual environment (https://docs.python.org/3/library/venv.html) + anaconda # conda environment (https://conda.io/) + pyenv # python environment (https://github.com/pyenv/pyenv) + goenv # go environment (https://github.com/syndbg/goenv) + nodenv # node.js version from nodenv (https://github.com/nodenv/nodenv) + nvm # node.js version from nvm (https://github.com/nvm-sh/nvm) + nodeenv # node.js environment (https://github.com/ekalinin/nodeenv) + # node_version # node.js version + # go_version # go version (https://golang.org) + # rust_version # rustc version (https://www.rust-lang.org) + # dotnet_version # .NET version (https://dotnet.microsoft.com) + # php_version # php version (https://www.php.net/) + # laravel_version # laravel php framework version (https://laravel.com/) + # java_version # java version (https://www.java.com/) + # package # name@version from package.json (https://docs.npmjs.com/files/package.json) + rbenv # ruby version from rbenv (https://github.com/rbenv/rbenv) + rvm # ruby version from rvm (https://rvm.io) + fvm # flutter version management (https://github.com/leoafarias/fvm) + luaenv # lua version from luaenv (https://github.com/cehoffman/luaenv) + jenv # java version from jenv (https://github.com/jenv/jenv) + plenv # perl version from plenv (https://github.com/tokuhirom/plenv) + perlbrew # perl version from perlbrew (https://github.com/gugod/App-perlbrew) + phpenv # php version from phpenv (https://github.com/phpenv/phpenv) + scalaenv # scala version from scalaenv (https://github.com/scalaenv/scalaenv) + haskell_stack # haskell version from stack (https://haskellstack.org/) + kubecontext # current kubernetes context (https://kubernetes.io/) + terraform # terraform workspace (https://www.terraform.io) + # terraform_version # terraform version (https://www.terraform.io) + aws # aws profile (https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html) + aws_eb_env # aws elastic beanstalk environment (https://aws.amazon.com/elasticbeanstalk/) + azure # azure account name (https://docs.microsoft.com/en-us/cli/azure) + gcloud # google cloud cli account and project (https://cloud.google.com/) + google_app_cred # google application credentials (https://cloud.google.com/docs/authentication/production) + toolbox # toolbox name (https://github.com/containers/toolbox) + context # user@hostname + nordvpn # nordvpn connection status, linux only (https://nordvpn.com/) + ranger # ranger shell (https://github.com/ranger/ranger) + yazi # yazi shell (https://github.com/sxyazi/yazi) + nnn # nnn shell (https://github.com/jarun/nnn) + lf # lf shell (https://github.com/gokcehan/lf) + xplr # xplr shell (https://github.com/sayanarijit/xplr) + vim_shell # vim shell indicator (:sh) + midnight_commander # midnight commander shell (https://midnight-commander.org/) + nix_shell # nix shell (https://nixos.org/nixos/nix-pills/developing-with-nix-shell.html) + chezmoi_shell # chezmoi shell (https://www.chezmoi.io/) + vi_mode # vi mode (you don't need this if you've enabled prompt_char) + # vpn_ip # virtual private network indicator + # load # CPU load + # disk_usage # disk usage + # ram # free RAM + # swap # used swap + todo # todo items (https://github.com/todotxt/todo.txt-cli) + timewarrior # timewarrior tracking status (https://timewarrior.net/) + taskwarrior # taskwarrior task count (https://taskwarrior.org/) + per_directory_history # Oh My Zsh per-directory-history local/global indicator + # cpu_arch # CPU architecture + time # current time + # =========================[ Line #2 ]========================= + newline + # ip # ip address and bandwidth usage for a specified network interface + # public_ip # public IP address + # proxy # system-wide http/https/ftp proxy + # battery # internal battery + # wifi # wifi speed + # example # example user-defined segment (see prompt_example function below) + ) + + # Defines character set used by powerlevel10k. It's best to let `p10k configure` set it for you. + typeset -g POWERLEVEL9K_MODE=awesome-fontconfig + # When set to `moderate`, some icons will have an extra space after them. This is meant to avoid + # icon overlap when using non-monospace fonts. When set to `none`, spaces are not added. + typeset -g POWERLEVEL9K_ICON_PADDING=moderate + + # When set to true, icons appear before content on both sides of the prompt. When set + # to false, icons go after content. If empty or not set, icons go before content in the left + # prompt and after content in the right prompt. + # + # You can also override it for a specific segment: + # + # POWERLEVEL9K_STATUS_ICON_BEFORE_CONTENT=false + # + # Or for a specific segment in specific state: + # + # POWERLEVEL9K_DIR_NOT_WRITABLE_ICON_BEFORE_CONTENT=false + typeset -g POWERLEVEL9K_ICON_BEFORE_CONTENT= + + # Add an empty line before each prompt. + typeset -g POWERLEVEL9K_PROMPT_ADD_NEWLINE=true + + # Connect left prompt lines with these symbols. You'll probably want to use the same color + # as POWERLEVEL9K_MULTILINE_FIRST_PROMPT_GAP_FOREGROUND below. + typeset -g POWERLEVEL9K_MULTILINE_FIRST_PROMPT_PREFIX='%240F╭─' + typeset -g POWERLEVEL9K_MULTILINE_NEWLINE_PROMPT_PREFIX='%240F├─' + typeset -g POWERLEVEL9K_MULTILINE_LAST_PROMPT_PREFIX='%240F╰─' + # Connect right prompt lines with these symbols. + typeset -g POWERLEVEL9K_MULTILINE_FIRST_PROMPT_SUFFIX='%240F─╮' + typeset -g POWERLEVEL9K_MULTILINE_NEWLINE_PROMPT_SUFFIX='%240F─┤' + typeset -g POWERLEVEL9K_MULTILINE_LAST_PROMPT_SUFFIX='%240F─╯' + + # Filler between left and right prompt on the first prompt line. You can set it to ' ', '·' or + # '─'. The last two make it easier to see the alignment between left and right prompt and to + # separate prompt from command output. You might want to set POWERLEVEL9K_PROMPT_ADD_NEWLINE=false + # for more compact prompt if using this option. + typeset -g POWERLEVEL9K_MULTILINE_FIRST_PROMPT_GAP_CHAR='─' + typeset -g POWERLEVEL9K_MULTILINE_FIRST_PROMPT_GAP_BACKGROUND= + typeset -g POWERLEVEL9K_MULTILINE_NEWLINE_PROMPT_GAP_BACKGROUND= + if [[ $POWERLEVEL9K_MULTILINE_FIRST_PROMPT_GAP_CHAR != ' ' ]]; then + # The color of the filler. You'll probably want to match the color of POWERLEVEL9K_MULTILINE + # ornaments defined above. + typeset -g POWERLEVEL9K_MULTILINE_FIRST_PROMPT_GAP_FOREGROUND=240 + # Start filler from the edge of the screen if there are no left segments on the first line. + typeset -g POWERLEVEL9K_EMPTY_LINE_LEFT_PROMPT_FIRST_SEGMENT_END_SYMBOL='%{%}' + # End filler on the edge of the screen if there are no right segments on the first line. + typeset -g POWERLEVEL9K_EMPTY_LINE_RIGHT_PROMPT_FIRST_SEGMENT_START_SYMBOL='%{%}' + fi + + # Separator between same-color segments on the left. + typeset -g POWERLEVEL9K_LEFT_SUBSEGMENT_SEPARATOR='\u2502' + # Separator between same-color segments on the right. + typeset -g POWERLEVEL9K_RIGHT_SUBSEGMENT_SEPARATOR='\u2502' + # Separator between different-color segments on the left. + typeset -g POWERLEVEL9K_LEFT_SEGMENT_SEPARATOR='' + # Separator between different-color segments on the right. + typeset -g POWERLEVEL9K_RIGHT_SEGMENT_SEPARATOR='' + # To remove a separator between two segments, add "_joined" to the second segment name. + # For example: POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS=(os_icon context_joined) + + # The right end of left prompt. + typeset -g POWERLEVEL9K_LEFT_PROMPT_LAST_SEGMENT_END_SYMBOL='\uE0B0' + # The left end of right prompt. + typeset -g POWERLEVEL9K_RIGHT_PROMPT_FIRST_SEGMENT_START_SYMBOL='\uE0B2' + # The left end of left prompt. + typeset -g POWERLEVEL9K_LEFT_PROMPT_FIRST_SEGMENT_START_SYMBOL='\uE0B2' + # The right end of right prompt. + typeset -g POWERLEVEL9K_RIGHT_PROMPT_LAST_SEGMENT_END_SYMBOL='\uE0B0' + # Left prompt terminator for lines without any segments. + typeset -g POWERLEVEL9K_EMPTY_LINE_LEFT_PROMPT_LAST_SEGMENT_END_SYMBOL= + + #################################[ os_icon: os identifier ]################################## + # OS identifier color. + typeset -g POWERLEVEL9K_OS_ICON_FOREGROUND=232 + typeset -g POWERLEVEL9K_OS_ICON_BACKGROUND=7 + # Custom icon. + # typeset -g POWERLEVEL9K_OS_ICON_CONTENT_EXPANSION='⭐' + + ################################[ prompt_char: prompt symbol ]################################ + # Transparent background. + typeset -g POWERLEVEL9K_PROMPT_CHAR_BACKGROUND= + # Green prompt symbol if the last command succeeded. + typeset -g POWERLEVEL9K_PROMPT_CHAR_OK_{VIINS,VICMD,VIVIS,VIOWR}_FOREGROUND=76 + # Red prompt symbol if the last command failed. + typeset -g POWERLEVEL9K_PROMPT_CHAR_ERROR_{VIINS,VICMD,VIVIS,VIOWR}_FOREGROUND=196 + # Default prompt symbol. + typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VIINS_CONTENT_EXPANSION='❯' + # Prompt symbol in command vi mode. + typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VICMD_CONTENT_EXPANSION='❮' + # Prompt symbol in visual vi mode. + typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VIVIS_CONTENT_EXPANSION='V' + # Prompt symbol in overwrite vi mode. + typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VIOWR_CONTENT_EXPANSION='▶' + typeset -g POWERLEVEL9K_PROMPT_CHAR_OVERWRITE_STATE=true + # No line terminator if prompt_char is the last segment. + typeset -g POWERLEVEL9K_PROMPT_CHAR_LEFT_PROMPT_LAST_SEGMENT_END_SYMBOL= + # No line introducer if prompt_char is the first segment. + typeset -g POWERLEVEL9K_PROMPT_CHAR_LEFT_PROMPT_FIRST_SEGMENT_START_SYMBOL= + # No surrounding whitespace. + typeset -g POWERLEVEL9K_PROMPT_CHAR_LEFT_{LEFT,RIGHT}_WHITESPACE= + + ##################################[ dir: current directory ]################################## + # Current directory background color. + typeset -g POWERLEVEL9K_DIR_BACKGROUND=4 + # Default current directory foreground color. + typeset -g POWERLEVEL9K_DIR_FOREGROUND=254 + # If directory is too long, shorten some of its segments to the shortest possible unique + # prefix. The shortened directory can be tab-completed to the original. + typeset -g POWERLEVEL9K_SHORTEN_STRATEGY=truncate_to_unique + # Replace removed segment suffixes with this symbol. + typeset -g POWERLEVEL9K_SHORTEN_DELIMITER= + # Color of the shortened directory segments. + typeset -g POWERLEVEL9K_DIR_SHORTENED_FOREGROUND=250 + # Color of the anchor directory segments. Anchor segments are never shortened. The first + # segment is always an anchor. + typeset -g POWERLEVEL9K_DIR_ANCHOR_FOREGROUND=255 + # Display anchor directory segments in bold. + typeset -g POWERLEVEL9K_DIR_ANCHOR_BOLD=true + # Don't shorten directories that contain any of these files. They are anchors. + local anchor_files=( + .bzr + .citc + .git + .hg + .node-version + .python-version + .go-version + .ruby-version + .lua-version + .java-version + .perl-version + .php-version + .tool-versions + .mise.toml + .shorten_folder_marker + .svn + .terraform + CVS + Cargo.toml + composer.json + go.mod + package.json + stack.yaml + ) + typeset -g POWERLEVEL9K_SHORTEN_FOLDER_MARKER="(${(j:|:)anchor_files})" + # If set to "first" ("last"), remove everything before the first (last) subdirectory that contains + # files matching $POWERLEVEL9K_SHORTEN_FOLDER_MARKER. For example, when the current directory is + # /foo/bar/git_repo/nested_git_repo/baz, prompt will display git_repo/nested_git_repo/baz (first) + # or nested_git_repo/baz (last). This assumes that git_repo and nested_git_repo contain markers + # and other directories don't. + # + # Optionally, "first" and "last" can be followed by ":" where is an integer. + # This moves the truncation point to the right (positive offset) or to the left (negative offset) + # relative to the marker. Plain "first" and "last" are equivalent to "first:0" and "last:0" + # respectively. + typeset -g POWERLEVEL9K_DIR_TRUNCATE_BEFORE_MARKER=false + # Don't shorten this many last directory segments. They are anchors. + typeset -g POWERLEVEL9K_SHORTEN_DIR_LENGTH=1 + # Shorten directory if it's longer than this even if there is space for it. The value can + # be either absolute (e.g., '80') or a percentage of terminal width (e.g, '50%'). If empty, + # directory will be shortened only when prompt doesn't fit or when other parameters demand it + # (see POWERLEVEL9K_DIR_MIN_COMMAND_COLUMNS and POWERLEVEL9K_DIR_MIN_COMMAND_COLUMNS_PCT below). + # If set to `0`, directory will always be shortened to its minimum length. + typeset -g POWERLEVEL9K_DIR_MAX_LENGTH=80 + # When `dir` segment is on the last prompt line, try to shorten it enough to leave at least this + # many columns for typing commands. + typeset -g POWERLEVEL9K_DIR_MIN_COMMAND_COLUMNS=40 + # When `dir` segment is on the last prompt line, try to shorten it enough to leave at least + # COLUMNS * POWERLEVEL9K_DIR_MIN_COMMAND_COLUMNS_PCT * 0.01 columns for typing commands. + typeset -g POWERLEVEL9K_DIR_MIN_COMMAND_COLUMNS_PCT=50 + # If set to true, embed a hyperlink into the directory. Useful for quickly + # opening a directory in the file manager simply by clicking the link. + # Can also be handy when the directory is shortened, as it allows you to see + # the full directory that was used in previous commands. + typeset -g POWERLEVEL9K_DIR_HYPERLINK=false + + # Enable special styling for non-writable and non-existent directories. See POWERLEVEL9K_LOCK_ICON + # and POWERLEVEL9K_DIR_CLASSES below. + typeset -g POWERLEVEL9K_DIR_SHOW_WRITABLE=v3 + + # The default icon shown next to non-writable and non-existent directories when + # POWERLEVEL9K_DIR_SHOW_WRITABLE is set to v3. + # typeset -g POWERLEVEL9K_LOCK_ICON='⭐' + + # POWERLEVEL9K_DIR_CLASSES allows you to specify custom icons and colors for different + # directories. It must be an array with 3 * N elements. Each triplet consists of: + # + # 1. A pattern against which the current directory ($PWD) is matched. Matching is done with + # extended_glob option enabled. + # 2. Directory class for the purpose of styling. + # 3. An empty string. + # + # Triplets are tried in order. The first triplet whose pattern matches $PWD wins. + # + # If POWERLEVEL9K_DIR_SHOW_WRITABLE is set to v3, non-writable and non-existent directories + # acquire class suffix _NOT_WRITABLE and NON_EXISTENT respectively. + # + # For example, given these settings: + # + # typeset -g POWERLEVEL9K_DIR_CLASSES=( + # '~/work(|/*)' WORK '' + # '~(|/*)' HOME '' + # '*' DEFAULT '') + # + # Whenever the current directory is ~/work or a subdirectory of ~/work, it gets styled with one + # of the following classes depending on its writability and existence: WORK, WORK_NOT_WRITABLE or + # WORK_NON_EXISTENT. + # + # Simply assigning classes to directories doesn't have any visible effects. It merely gives you an + # option to define custom colors and icons for different directory classes. + # + # # Styling for WORK. + # typeset -g POWERLEVEL9K_DIR_WORK_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_DIR_WORK_BACKGROUND=4 + # typeset -g POWERLEVEL9K_DIR_WORK_FOREGROUND=254 + # typeset -g POWERLEVEL9K_DIR_WORK_SHORTENED_FOREGROUND=250 + # typeset -g POWERLEVEL9K_DIR_WORK_ANCHOR_FOREGROUND=255 + # + # # Styling for WORK_NOT_WRITABLE. + # typeset -g POWERLEVEL9K_DIR_WORK_NOT_WRITABLE_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_DIR_WORK_NOT_WRITABLE_BACKGROUND=4 + # typeset -g POWERLEVEL9K_DIR_WORK_NOT_WRITABLE_FOREGROUND=254 + # typeset -g POWERLEVEL9K_DIR_WORK_NOT_WRITABLE_SHORTENED_FOREGROUND=250 + # typeset -g POWERLEVEL9K_DIR_WORK_NOT_WRITABLE_ANCHOR_FOREGROUND=255 + # + # # Styling for WORK_NON_EXISTENT. + # typeset -g POWERLEVEL9K_DIR_WORK_NON_EXISTENT_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_DIR_WORK_NON_EXISTENT_BACKGROUND=4 + # typeset -g POWERLEVEL9K_DIR_WORK_NON_EXISTENT_FOREGROUND=254 + # typeset -g POWERLEVEL9K_DIR_WORK_NON_EXISTENT_SHORTENED_FOREGROUND=250 + # typeset -g POWERLEVEL9K_DIR_WORK_NON_EXISTENT_ANCHOR_FOREGROUND=255 + # + # If a styling parameter isn't explicitly defined for some class, it falls back to the classless + # parameter. For example, if POWERLEVEL9K_DIR_WORK_NOT_WRITABLE_FOREGROUND is not set, it falls + # back to POWERLEVEL9K_DIR_FOREGROUND. + # + # typeset -g POWERLEVEL9K_DIR_CLASSES=() + + # Custom prefix. + # typeset -g POWERLEVEL9K_DIR_PREFIX='in ' + + #####################################[ vcs: git status ]###################################### + # Version control background colors. + typeset -g POWERLEVEL9K_VCS_CLEAN_BACKGROUND=2 + typeset -g POWERLEVEL9K_VCS_MODIFIED_BACKGROUND=3 + typeset -g POWERLEVEL9K_VCS_UNTRACKED_BACKGROUND=2 + typeset -g POWERLEVEL9K_VCS_CONFLICTED_BACKGROUND=3 + typeset -g POWERLEVEL9K_VCS_LOADING_BACKGROUND=8 + + # Branch icon. Set this parameter to '\UE0A0 ' for the popular Powerline branch icon. + typeset -g POWERLEVEL9K_VCS_BRANCH_ICON='\uF126 ' + + # Untracked files icon. It's really a question mark, your font isn't broken. + # Change the value of this parameter to show a different icon. + typeset -g POWERLEVEL9K_VCS_UNTRACKED_ICON='?' + + # Formatter for Git status. + # + # Example output: master wip ⇣42⇡42 *42 merge ~42 +42 !42 ?42. + # + # You can edit the function to customize how Git status looks. + # + # VCS_STATUS_* parameters are set by gitstatus plugin. See reference: + # https://github.com/romkatv/gitstatus/blob/master/gitstatus.plugin.zsh. + function my_git_formatter() { + emulate -L zsh + + if [[ -n $P9K_CONTENT ]]; then + # If P9K_CONTENT is not empty, use it. It's either "loading" or from vcs_info (not from + # gitstatus plugin). VCS_STATUS_* parameters are not available in this case. + typeset -g my_git_format=$P9K_CONTENT + return + fi + + # Styling for different parts of Git status. + local meta='%7F' # white foreground + local clean='%0F' # black foreground + local modified='%0F' # black foreground + local untracked='%0F' # black foreground + local conflicted='%1F' # red foreground + + local res + + if [[ -n $VCS_STATUS_LOCAL_BRANCH ]]; then + local branch=${(V)VCS_STATUS_LOCAL_BRANCH} + # If local branch name is at most 32 characters long, show it in full. + # Otherwise show the first 12 … the last 12. + # Tip: To always show local branch name in full without truncation, delete the next line. + (( $#branch > 32 )) && branch[13,-13]="…" # <-- this line + res+="${clean}${(g::)POWERLEVEL9K_VCS_BRANCH_ICON}${branch//\%/%%}" + fi + + if [[ -n $VCS_STATUS_TAG + # Show tag only if not on a branch. + # Tip: To always show tag, delete the next line. + && -z $VCS_STATUS_LOCAL_BRANCH # <-- this line + ]]; then + local tag=${(V)VCS_STATUS_TAG} + # If tag name is at most 32 characters long, show it in full. + # Otherwise show the first 12 … the last 12. + # Tip: To always show tag name in full without truncation, delete the next line. + (( $#tag > 32 )) && tag[13,-13]="…" # <-- this line + res+="${meta}#${clean}${tag//\%/%%}" + fi + + # Display the current Git commit if there is no branch and no tag. + # Tip: To always display the current Git commit, delete the next line. + [[ -z $VCS_STATUS_LOCAL_BRANCH && -z $VCS_STATUS_TAG ]] && # <-- this line + res+="${meta}@${clean}${VCS_STATUS_COMMIT[1,8]}" + + # Show tracking branch name if it differs from local branch. + if [[ -n ${VCS_STATUS_REMOTE_BRANCH:#$VCS_STATUS_LOCAL_BRANCH} ]]; then + res+="${meta}:${clean}${(V)VCS_STATUS_REMOTE_BRANCH//\%/%%}" + fi + + # Display "wip" if the latest commit's summary contains "wip" or "WIP". + if [[ $VCS_STATUS_COMMIT_SUMMARY == (|*[^[:alnum:]])(wip|WIP)(|[^[:alnum:]]*) ]]; then + res+=" ${modified}wip" + fi + + if (( VCS_STATUS_COMMITS_AHEAD || VCS_STATUS_COMMITS_BEHIND )); then + # ⇣42 if behind the remote. + (( VCS_STATUS_COMMITS_BEHIND )) && res+=" ${clean}⇣${VCS_STATUS_COMMITS_BEHIND}" + # ⇡42 if ahead of the remote; no leading space if also behind the remote: ⇣42⇡42. + (( VCS_STATUS_COMMITS_AHEAD && !VCS_STATUS_COMMITS_BEHIND )) && res+=" " + (( VCS_STATUS_COMMITS_AHEAD )) && res+="${clean}⇡${VCS_STATUS_COMMITS_AHEAD}" + elif [[ -n $VCS_STATUS_REMOTE_BRANCH ]]; then + # Tip: Uncomment the next line to display '=' if up to date with the remote. + # res+=" ${clean}=" + fi + + # ⇠42 if behind the push remote. + (( VCS_STATUS_PUSH_COMMITS_BEHIND )) && res+=" ${clean}⇠${VCS_STATUS_PUSH_COMMITS_BEHIND}" + (( VCS_STATUS_PUSH_COMMITS_AHEAD && !VCS_STATUS_PUSH_COMMITS_BEHIND )) && res+=" " + # ⇢42 if ahead of the push remote; no leading space if also behind: ⇠42⇢42. + (( VCS_STATUS_PUSH_COMMITS_AHEAD )) && res+="${clean}⇢${VCS_STATUS_PUSH_COMMITS_AHEAD}" + # *42 if have stashes. + (( VCS_STATUS_STASHES )) && res+=" ${clean}*${VCS_STATUS_STASHES}" + # 'merge' if the repo is in an unusual state. + [[ -n $VCS_STATUS_ACTION ]] && res+=" ${conflicted}${VCS_STATUS_ACTION}" + # ~42 if have merge conflicts. + (( VCS_STATUS_NUM_CONFLICTED )) && res+=" ${conflicted}~${VCS_STATUS_NUM_CONFLICTED}" + # +42 if have staged changes. + (( VCS_STATUS_NUM_STAGED )) && res+=" ${modified}+${VCS_STATUS_NUM_STAGED}" + # !42 if have unstaged changes. + (( VCS_STATUS_NUM_UNSTAGED )) && res+=" ${modified}!${VCS_STATUS_NUM_UNSTAGED}" + # ?42 if have untracked files. It's really a question mark, your font isn't broken. + # See POWERLEVEL9K_VCS_UNTRACKED_ICON above if you want to use a different icon. + # Remove the next line if you don't want to see untracked files at all. + (( VCS_STATUS_NUM_UNTRACKED )) && res+=" ${untracked}${(g::)POWERLEVEL9K_VCS_UNTRACKED_ICON}${VCS_STATUS_NUM_UNTRACKED}" + # "─" if the number of unstaged files is unknown. This can happen due to + # POWERLEVEL9K_VCS_MAX_INDEX_SIZE_DIRTY (see below) being set to a non-negative number lower + # than the number of files in the Git index, or due to bash.showDirtyState being set to false + # in the repository config. The number of staged and untracked files may also be unknown + # in this case. + (( VCS_STATUS_HAS_UNSTAGED == -1 )) && res+=" ${modified}─" + + typeset -g my_git_format=$res + } + functions -M my_git_formatter 2>/dev/null + + # Don't count the number of unstaged, untracked and conflicted files in Git repositories with + # more than this many files in the index. Negative value means infinity. + # + # If you are working in Git repositories with tens of millions of files and seeing performance + # sagging, try setting POWERLEVEL9K_VCS_MAX_INDEX_SIZE_DIRTY to a number lower than the output + # of `git ls-files | wc -l`. Alternatively, add `bash.showDirtyState = false` to the repository's + # config: `git config bash.showDirtyState false`. + typeset -g POWERLEVEL9K_VCS_MAX_INDEX_SIZE_DIRTY=-1 + + # Don't show Git status in prompt for repositories whose workdir matches this pattern. + # For example, if set to '~', the Git repository at $HOME/.git will be ignored. + # Multiple patterns can be combined with '|': '~(|/foo)|/bar/baz/*'. + typeset -g POWERLEVEL9K_VCS_DISABLED_WORKDIR_PATTERN='~' + + # Disable the default Git status formatting. + typeset -g POWERLEVEL9K_VCS_DISABLE_GITSTATUS_FORMATTING=true + # Install our own Git status formatter. + typeset -g POWERLEVEL9K_VCS_CONTENT_EXPANSION='${$((my_git_formatter()))+${my_git_format}}' + # Enable counters for staged, unstaged, etc. + typeset -g POWERLEVEL9K_VCS_{STAGED,UNSTAGED,UNTRACKED,CONFLICTED,COMMITS_AHEAD,COMMITS_BEHIND}_MAX_NUM=-1 + + # Custom icon. + # typeset -g POWERLEVEL9K_VCS_VISUAL_IDENTIFIER_EXPANSION='⭐' + # Custom prefix. + # typeset -g POWERLEVEL9K_VCS_PREFIX='on ' + + # Show status of repositories of these types. You can add svn and/or hg if you are + # using them. If you do, your prompt may become slow even when your current directory + # isn't in an svn or hg repository. + typeset -g POWERLEVEL9K_VCS_BACKENDS=(git) + + ##########################[ status: exit code of the last command ]########################### + # Enable OK_PIPE, ERROR_PIPE and ERROR_SIGNAL status states to allow us to enable, disable and + # style them independently from the regular OK and ERROR state. + typeset -g POWERLEVEL9K_STATUS_EXTENDED_STATES=true + + # Status on success. No content, just an icon. No need to show it if prompt_char is enabled as + # it will signify success by turning green. + typeset -g POWERLEVEL9K_STATUS_OK=true + typeset -g POWERLEVEL9K_STATUS_OK_VISUAL_IDENTIFIER_EXPANSION='✔' + typeset -g POWERLEVEL9K_STATUS_OK_FOREGROUND=2 + typeset -g POWERLEVEL9K_STATUS_OK_BACKGROUND=0 + + # Status when some part of a pipe command fails but the overall exit status is zero. It may look + # like this: 1|0. + typeset -g POWERLEVEL9K_STATUS_OK_PIPE=true + typeset -g POWERLEVEL9K_STATUS_OK_PIPE_VISUAL_IDENTIFIER_EXPANSION='✔' + typeset -g POWERLEVEL9K_STATUS_OK_PIPE_FOREGROUND=2 + typeset -g POWERLEVEL9K_STATUS_OK_PIPE_BACKGROUND=0 + + # Status when it's just an error code (e.g., '1'). No need to show it if prompt_char is enabled as + # it will signify error by turning red. + typeset -g POWERLEVEL9K_STATUS_ERROR=true + typeset -g POWERLEVEL9K_STATUS_ERROR_VISUAL_IDENTIFIER_EXPANSION='✘' + typeset -g POWERLEVEL9K_STATUS_ERROR_FOREGROUND=3 + typeset -g POWERLEVEL9K_STATUS_ERROR_BACKGROUND=1 + + # Status when the last command was terminated by a signal. + typeset -g POWERLEVEL9K_STATUS_ERROR_SIGNAL=true + # Use terse signal names: "INT" instead of "SIGINT(2)". + typeset -g POWERLEVEL9K_STATUS_VERBOSE_SIGNAME=false + typeset -g POWERLEVEL9K_STATUS_ERROR_SIGNAL_VISUAL_IDENTIFIER_EXPANSION='✘' + typeset -g POWERLEVEL9K_STATUS_ERROR_SIGNAL_FOREGROUND=3 + typeset -g POWERLEVEL9K_STATUS_ERROR_SIGNAL_BACKGROUND=1 + + # Status when some part of a pipe command fails and the overall exit status is also non-zero. + # It may look like this: 1|0. + typeset -g POWERLEVEL9K_STATUS_ERROR_PIPE=true + typeset -g POWERLEVEL9K_STATUS_ERROR_PIPE_VISUAL_IDENTIFIER_EXPANSION='✘' + typeset -g POWERLEVEL9K_STATUS_ERROR_PIPE_FOREGROUND=3 + typeset -g POWERLEVEL9K_STATUS_ERROR_PIPE_BACKGROUND=1 + + ###################[ command_execution_time: duration of the last command ]################### + # Execution time color. + typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_FOREGROUND=0 + typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_BACKGROUND=3 + # Show duration of the last command if takes at least this many seconds. + typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_THRESHOLD=3 + # Show this many fractional digits. Zero means round to seconds. + typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_PRECISION=0 + # Duration format: 1d 2h 3m 4s. + typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_FORMAT='d h m s' + # Custom icon. + # typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_VISUAL_IDENTIFIER_EXPANSION='⭐' + # Custom prefix. + # typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_PREFIX='took ' + + #######################[ background_jobs: presence of background jobs ]####################### + # Background jobs color. + typeset -g POWERLEVEL9K_BACKGROUND_JOBS_FOREGROUND=6 + typeset -g POWERLEVEL9K_BACKGROUND_JOBS_BACKGROUND=0 + # Don't show the number of background jobs. + typeset -g POWERLEVEL9K_BACKGROUND_JOBS_VERBOSE=false + # Custom icon. + # typeset -g POWERLEVEL9K_BACKGROUND_JOBS_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #######################[ direnv: direnv status (https://direnv.net/) ]######################## + # Direnv color. + typeset -g POWERLEVEL9K_DIRENV_FOREGROUND=3 + typeset -g POWERLEVEL9K_DIRENV_BACKGROUND=0 + # Custom icon. + # typeset -g POWERLEVEL9K_DIRENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###############[ asdf: asdf version manager (https://github.com/asdf-vm/asdf) ]############### + # Default asdf color. Only used to display tools for which there is no color override (see below). + # Tip: Override these parameters for ${TOOL} with POWERLEVEL9K_ASDF_${TOOL}_FOREGROUND and + # POWERLEVEL9K_ASDF_${TOOL}_BACKGROUND. + typeset -g POWERLEVEL9K_ASDF_FOREGROUND=0 + typeset -g POWERLEVEL9K_ASDF_BACKGROUND=7 + + # There are four parameters that can be used to hide asdf tools. Each parameter describes + # conditions under which a tool gets hidden. Parameters can hide tools but not unhide them. If at + # least one parameter decides to hide a tool, that tool gets hidden. If no parameter decides to + # hide a tool, it gets shown. + # + # Special note on the difference between POWERLEVEL9K_ASDF_SOURCES and + # POWERLEVEL9K_ASDF_PROMPT_ALWAYS_SHOW. Consider the effect of the following commands: + # + # asdf local python 3.8.1 + # asdf global python 3.8.1 + # + # After running both commands the current python version is 3.8.1 and its source is "local" as + # it takes precedence over "global". If POWERLEVEL9K_ASDF_PROMPT_ALWAYS_SHOW is set to false, + # it'll hide python version in this case because 3.8.1 is the same as the global version. + # POWERLEVEL9K_ASDF_SOURCES will hide python version only if the value of this parameter doesn't + # contain "local". + + # Hide tool versions that don't come from one of these sources. + # + # Available sources: + # + # - shell `asdf current` says "set by ASDF_${TOOL}_VERSION environment variable" + # - local `asdf current` says "set by /some/not/home/directory/file" + # - global `asdf current` says "set by /home/username/file" + # + # Note: If this parameter is set to (shell local global), it won't hide tools. + # Tip: Override this parameter for ${TOOL} with POWERLEVEL9K_ASDF_${TOOL}_SOURCES. + typeset -g POWERLEVEL9K_ASDF_SOURCES=(shell local global) + + # If set to false, hide tool versions that are the same as global. + # + # Note: The name of this parameter doesn't reflect its meaning at all. + # Note: If this parameter is set to true, it won't hide tools. + # Tip: Override this parameter for ${TOOL} with POWERLEVEL9K_ASDF_${TOOL}_PROMPT_ALWAYS_SHOW. + typeset -g POWERLEVEL9K_ASDF_PROMPT_ALWAYS_SHOW=false + + # If set to false, hide tool versions that are equal to "system". + # + # Note: If this parameter is set to true, it won't hide tools. + # Tip: Override this parameter for ${TOOL} with POWERLEVEL9K_ASDF_${TOOL}_SHOW_SYSTEM. + typeset -g POWERLEVEL9K_ASDF_SHOW_SYSTEM=true + + # If set to non-empty value, hide tools unless there is a file matching the specified file pattern + # in the current directory, or its parent directory, or its grandparent directory, and so on. + # + # Note: If this parameter is set to empty value, it won't hide tools. + # Note: SHOW_ON_UPGLOB isn't specific to asdf. It works with all prompt segments. + # Tip: Override this parameter for ${TOOL} with POWERLEVEL9K_ASDF_${TOOL}_SHOW_ON_UPGLOB. + # + # Example: Hide nodejs version when there is no package.json and no *.js files in the current + # directory, in `..`, in `../..` and so on. + # + # typeset -g POWERLEVEL9K_ASDF_NODEJS_SHOW_ON_UPGLOB='*.js|package.json' + typeset -g POWERLEVEL9K_ASDF_SHOW_ON_UPGLOB= + + # Ruby version from asdf. + typeset -g POWERLEVEL9K_ASDF_RUBY_FOREGROUND=0 + typeset -g POWERLEVEL9K_ASDF_RUBY_BACKGROUND=1 + # typeset -g POWERLEVEL9K_ASDF_RUBY_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_RUBY_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Python version from asdf. + typeset -g POWERLEVEL9K_ASDF_PYTHON_FOREGROUND=0 + typeset -g POWERLEVEL9K_ASDF_PYTHON_BACKGROUND=4 + # typeset -g POWERLEVEL9K_ASDF_PYTHON_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_PYTHON_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Go version from asdf. + typeset -g POWERLEVEL9K_ASDF_GOLANG_FOREGROUND=0 + typeset -g POWERLEVEL9K_ASDF_GOLANG_BACKGROUND=4 + # typeset -g POWERLEVEL9K_ASDF_GOLANG_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_GOLANG_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Node.js version from asdf. + typeset -g POWERLEVEL9K_ASDF_NODEJS_FOREGROUND=0 + typeset -g POWERLEVEL9K_ASDF_NODEJS_BACKGROUND=2 + # typeset -g POWERLEVEL9K_ASDF_NODEJS_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_NODEJS_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Rust version from asdf. + typeset -g POWERLEVEL9K_ASDF_RUST_FOREGROUND=0 + typeset -g POWERLEVEL9K_ASDF_RUST_BACKGROUND=208 + # typeset -g POWERLEVEL9K_ASDF_RUST_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_RUST_SHOW_ON_UPGLOB='*.foo|*.bar' + + # .NET Core version from asdf. + typeset -g POWERLEVEL9K_ASDF_DOTNET_CORE_FOREGROUND=0 + typeset -g POWERLEVEL9K_ASDF_DOTNET_CORE_BACKGROUND=5 + # typeset -g POWERLEVEL9K_ASDF_DOTNET_CORE_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_DOTNET_CORE_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Flutter version from asdf. + typeset -g POWERLEVEL9K_ASDF_FLUTTER_FOREGROUND=0 + typeset -g POWERLEVEL9K_ASDF_FLUTTER_BACKGROUND=4 + # typeset -g POWERLEVEL9K_ASDF_FLUTTER_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_FLUTTER_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Lua version from asdf. + typeset -g POWERLEVEL9K_ASDF_LUA_FOREGROUND=0 + typeset -g POWERLEVEL9K_ASDF_LUA_BACKGROUND=4 + # typeset -g POWERLEVEL9K_ASDF_LUA_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_LUA_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Java version from asdf. + typeset -g POWERLEVEL9K_ASDF_JAVA_FOREGROUND=1 + typeset -g POWERLEVEL9K_ASDF_JAVA_BACKGROUND=7 + # typeset -g POWERLEVEL9K_ASDF_JAVA_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_JAVA_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Perl version from asdf. + typeset -g POWERLEVEL9K_ASDF_PERL_FOREGROUND=0 + typeset -g POWERLEVEL9K_ASDF_PERL_BACKGROUND=4 + # typeset -g POWERLEVEL9K_ASDF_PERL_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_PERL_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Erlang version from asdf. + typeset -g POWERLEVEL9K_ASDF_ERLANG_FOREGROUND=0 + typeset -g POWERLEVEL9K_ASDF_ERLANG_BACKGROUND=1 + # typeset -g POWERLEVEL9K_ASDF_ERLANG_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_ERLANG_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Elixir version from asdf. + typeset -g POWERLEVEL9K_ASDF_ELIXIR_FOREGROUND=0 + typeset -g POWERLEVEL9K_ASDF_ELIXIR_BACKGROUND=5 + # typeset -g POWERLEVEL9K_ASDF_ELIXIR_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_ELIXIR_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Postgres version from asdf. + typeset -g POWERLEVEL9K_ASDF_POSTGRES_FOREGROUND=0 + typeset -g POWERLEVEL9K_ASDF_POSTGRES_BACKGROUND=6 + # typeset -g POWERLEVEL9K_ASDF_POSTGRES_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_POSTGRES_SHOW_ON_UPGLOB='*.foo|*.bar' + + # PHP version from asdf. + typeset -g POWERLEVEL9K_ASDF_PHP_FOREGROUND=0 + typeset -g POWERLEVEL9K_ASDF_PHP_BACKGROUND=5 + # typeset -g POWERLEVEL9K_ASDF_PHP_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_PHP_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Haskell version from asdf. + typeset -g POWERLEVEL9K_ASDF_HASKELL_FOREGROUND=0 + typeset -g POWERLEVEL9K_ASDF_HASKELL_BACKGROUND=3 + # typeset -g POWERLEVEL9K_ASDF_HASKELL_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_HASKELL_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Julia version from asdf. + typeset -g POWERLEVEL9K_ASDF_JULIA_FOREGROUND=0 + typeset -g POWERLEVEL9K_ASDF_JULIA_BACKGROUND=2 + # typeset -g POWERLEVEL9K_ASDF_JULIA_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_JULIA_SHOW_ON_UPGLOB='*.foo|*.bar' + + ##########[ nordvpn: nordvpn connection status, linux only (https://nordvpn.com/) ]########### + # NordVPN connection indicator color. + typeset -g POWERLEVEL9K_NORDVPN_FOREGROUND=7 + typeset -g POWERLEVEL9K_NORDVPN_BACKGROUND=4 + # Hide NordVPN connection indicator when not connected. + typeset -g POWERLEVEL9K_NORDVPN_{DISCONNECTED,CONNECTING,DISCONNECTING}_CONTENT_EXPANSION= + typeset -g POWERLEVEL9K_NORDVPN_{DISCONNECTED,CONNECTING,DISCONNECTING}_VISUAL_IDENTIFIER_EXPANSION= + # Custom icon. + # typeset -g POWERLEVEL9K_NORDVPN_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #################[ ranger: ranger shell (https://github.com/ranger/ranger) ]################## + # Ranger shell color. + typeset -g POWERLEVEL9K_RANGER_FOREGROUND=3 + typeset -g POWERLEVEL9K_RANGER_BACKGROUND=0 + # Custom icon. + # typeset -g POWERLEVEL9K_RANGER_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ####################[ yazi: yazi shell (https://github.com/sxyazi/yazi) ]##################### + # Yazi shell color. + typeset -g POWERLEVEL9K_YAZI_FOREGROUND=3 + typeset -g POWERLEVEL9K_YAZI_BACKGROUND=0 + # Custom icon. + # typeset -g POWERLEVEL9K_YAZI_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ######################[ nnn: nnn shell (https://github.com/jarun/nnn) ]####################### + # Nnn shell color. + typeset -g POWERLEVEL9K_NNN_FOREGROUND=0 + typeset -g POWERLEVEL9K_NNN_BACKGROUND=6 + # Custom icon. + # typeset -g POWERLEVEL9K_NNN_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ######################[ lf: lf shell (https://github.com/gokcehan/lf) ]####################### + # lf shell color. + typeset -g POWERLEVEL9K_LF_FOREGROUND=0 + typeset -g POWERLEVEL9K_LF_BACKGROUND=6 + # Custom icon. + # typeset -g POWERLEVEL9K_LF_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##################[ xplr: xplr shell (https://github.com/sayanarijit/xplr) ]################## + # xplr shell color. + typeset -g POWERLEVEL9K_XPLR_FOREGROUND=0 + typeset -g POWERLEVEL9K_XPLR_BACKGROUND=6 + # Custom icon. + # typeset -g POWERLEVEL9K_XPLR_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###########################[ vim_shell: vim shell indicator (:sh) ]########################### + # Vim shell indicator color. + typeset -g POWERLEVEL9K_VIM_SHELL_FOREGROUND=0 + typeset -g POWERLEVEL9K_VIM_SHELL_BACKGROUND=2 + # Custom icon. + # typeset -g POWERLEVEL9K_VIM_SHELL_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ######[ midnight_commander: midnight commander shell (https://midnight-commander.org/) ]###### + # Midnight Commander shell color. + typeset -g POWERLEVEL9K_MIDNIGHT_COMMANDER_FOREGROUND=3 + typeset -g POWERLEVEL9K_MIDNIGHT_COMMANDER_BACKGROUND=0 + # Custom icon. + # typeset -g POWERLEVEL9K_MIDNIGHT_COMMANDER_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #[ nix_shell: nix shell (https://nixos.org/nixos/nix-pills/developing-with-nix-shell.html) ]## + # Nix shell color. + typeset -g POWERLEVEL9K_NIX_SHELL_FOREGROUND=0 + typeset -g POWERLEVEL9K_NIX_SHELL_BACKGROUND=4 + + # Display the icon of nix_shell if PATH contains a subdirectory of /nix/store. + # typeset -g POWERLEVEL9K_NIX_SHELL_INFER_FROM_PATH=false + + # Tip: If you want to see just the icon without "pure" and "impure", uncomment the next line. + # typeset -g POWERLEVEL9K_NIX_SHELL_CONTENT_EXPANSION= + + # Custom icon. + # typeset -g POWERLEVEL9K_NIX_SHELL_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##################[ chezmoi_shell: chezmoi shell (https://www.chezmoi.io/) ]################## + # chezmoi shell color. + typeset -g POWERLEVEL9K_CHEZMOI_SHELL_FOREGROUND=0 + typeset -g POWERLEVEL9K_CHEZMOI_SHELL_BACKGROUND=4 + # Custom icon. + # typeset -g POWERLEVEL9K_CHEZMOI_SHELL_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##################################[ disk_usage: disk usage ]################################## + # Colors for different levels of disk usage. + typeset -g POWERLEVEL9K_DISK_USAGE_NORMAL_FOREGROUND=3 + typeset -g POWERLEVEL9K_DISK_USAGE_NORMAL_BACKGROUND=0 + typeset -g POWERLEVEL9K_DISK_USAGE_WARNING_FOREGROUND=0 + typeset -g POWERLEVEL9K_DISK_USAGE_WARNING_BACKGROUND=3 + typeset -g POWERLEVEL9K_DISK_USAGE_CRITICAL_FOREGROUND=7 + typeset -g POWERLEVEL9K_DISK_USAGE_CRITICAL_BACKGROUND=1 + # Thresholds for different levels of disk usage (percentage points). + typeset -g POWERLEVEL9K_DISK_USAGE_WARNING_LEVEL=90 + typeset -g POWERLEVEL9K_DISK_USAGE_CRITICAL_LEVEL=95 + # If set to true, hide disk usage when below $POWERLEVEL9K_DISK_USAGE_WARNING_LEVEL percent. + typeset -g POWERLEVEL9K_DISK_USAGE_ONLY_WARNING=false + # Custom icon. + # typeset -g POWERLEVEL9K_DISK_USAGE_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###########[ vi_mode: vi mode (you don't need this if you've enabled prompt_char) ]########### + # Foreground color. + typeset -g POWERLEVEL9K_VI_MODE_FOREGROUND=0 + # Text and color for normal (a.k.a. command) vi mode. + typeset -g POWERLEVEL9K_VI_COMMAND_MODE_STRING=NORMAL + typeset -g POWERLEVEL9K_VI_MODE_NORMAL_BACKGROUND=2 + # Text and color for visual vi mode. + typeset -g POWERLEVEL9K_VI_VISUAL_MODE_STRING=VISUAL + typeset -g POWERLEVEL9K_VI_MODE_VISUAL_BACKGROUND=4 + # Text and color for overtype (a.k.a. overwrite and replace) vi mode. + typeset -g POWERLEVEL9K_VI_OVERWRITE_MODE_STRING=OVERTYPE + typeset -g POWERLEVEL9K_VI_MODE_OVERWRITE_BACKGROUND=3 + # Text and color for insert vi mode. + typeset -g POWERLEVEL9K_VI_INSERT_MODE_STRING= + typeset -g POWERLEVEL9K_VI_MODE_INSERT_FOREGROUND=8 + # Custom icon. + # typeset -g POWERLEVEL9K_VI_MODE_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ######################################[ ram: free RAM ]####################################### + # RAM color. + typeset -g POWERLEVEL9K_RAM_FOREGROUND=0 + typeset -g POWERLEVEL9K_RAM_BACKGROUND=3 + # Custom icon. + # typeset -g POWERLEVEL9K_RAM_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #####################################[ swap: used swap ]###################################### + # Swap color. + typeset -g POWERLEVEL9K_SWAP_FOREGROUND=0 + typeset -g POWERLEVEL9K_SWAP_BACKGROUND=3 + # Custom icon. + # typeset -g POWERLEVEL9K_SWAP_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ######################################[ load: CPU load ]###################################### + # Show average CPU load over this many last minutes. Valid values are 1, 5 and 15. + typeset -g POWERLEVEL9K_LOAD_WHICH=5 + # Load color when load is under 50%. + typeset -g POWERLEVEL9K_LOAD_NORMAL_FOREGROUND=0 + typeset -g POWERLEVEL9K_LOAD_NORMAL_BACKGROUND=2 + # Load color when load is between 50% and 70%. + typeset -g POWERLEVEL9K_LOAD_WARNING_FOREGROUND=0 + typeset -g POWERLEVEL9K_LOAD_WARNING_BACKGROUND=3 + # Load color when load is over 70%. + typeset -g POWERLEVEL9K_LOAD_CRITICAL_FOREGROUND=0 + typeset -g POWERLEVEL9K_LOAD_CRITICAL_BACKGROUND=1 + # Custom icon. + # typeset -g POWERLEVEL9K_LOAD_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ################[ todo: todo items (https://github.com/todotxt/todo.txt-cli) ]################ + # Todo color. + typeset -g POWERLEVEL9K_TODO_FOREGROUND=0 + typeset -g POWERLEVEL9K_TODO_BACKGROUND=8 + # Hide todo when the total number of tasks is zero. + typeset -g POWERLEVEL9K_TODO_HIDE_ZERO_TOTAL=true + # Hide todo when the number of tasks after filtering is zero. + typeset -g POWERLEVEL9K_TODO_HIDE_ZERO_FILTERED=false + + # Todo format. The following parameters are available within the expansion. + # + # - P9K_TODO_TOTAL_TASK_COUNT The total number of tasks. + # - P9K_TODO_FILTERED_TASK_COUNT The number of tasks after filtering. + # + # These variables correspond to the last line of the output of `todo.sh -p ls`: + # + # TODO: 24 of 42 tasks shown + # + # Here 24 is P9K_TODO_FILTERED_TASK_COUNT and 42 is P9K_TODO_TOTAL_TASK_COUNT. + # + # typeset -g POWERLEVEL9K_TODO_CONTENT_EXPANSION='$P9K_TODO_FILTERED_TASK_COUNT' + + # Custom icon. + # typeset -g POWERLEVEL9K_TODO_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###########[ timewarrior: timewarrior tracking status (https://timewarrior.net/) ]############ + # Timewarrior color. + typeset -g POWERLEVEL9K_TIMEWARRIOR_FOREGROUND=255 + typeset -g POWERLEVEL9K_TIMEWARRIOR_BACKGROUND=8 + + # If the tracked task is longer than 24 characters, truncate and append "…". + # Tip: To always display tasks without truncation, delete the following parameter. + # Tip: To hide task names and display just the icon when time tracking is enabled, set the + # value of the following parameter to "". + typeset -g POWERLEVEL9K_TIMEWARRIOR_CONTENT_EXPANSION='${P9K_CONTENT:0:24}${${P9K_CONTENT:24}:+…}' + + # Custom icon. + # typeset -g POWERLEVEL9K_TIMEWARRIOR_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##############[ taskwarrior: taskwarrior task count (https://taskwarrior.org/) ]############## + # Taskwarrior color. + typeset -g POWERLEVEL9K_TASKWARRIOR_FOREGROUND=0 + typeset -g POWERLEVEL9K_TASKWARRIOR_BACKGROUND=6 + + # Taskwarrior segment format. The following parameters are available within the expansion. + # + # - P9K_TASKWARRIOR_PENDING_COUNT The number of pending tasks: `task +PENDING count`. + # - P9K_TASKWARRIOR_OVERDUE_COUNT The number of overdue tasks: `task +OVERDUE count`. + # + # Zero values are represented as empty parameters. + # + # The default format: + # + # '${P9K_TASKWARRIOR_OVERDUE_COUNT:+"!$P9K_TASKWARRIOR_OVERDUE_COUNT/"}$P9K_TASKWARRIOR_PENDING_COUNT' + # + # typeset -g POWERLEVEL9K_TASKWARRIOR_CONTENT_EXPANSION='$P9K_TASKWARRIOR_PENDING_COUNT' + + # Custom icon. + # typeset -g POWERLEVEL9K_TASKWARRIOR_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ######[ per_directory_history: Oh My Zsh per-directory-history local/global indicator ]####### + # Color when using local/global history. + typeset -g POWERLEVEL9K_PER_DIRECTORY_HISTORY_LOCAL_FOREGROUND=0 + typeset -g POWERLEVEL9K_PER_DIRECTORY_HISTORY_LOCAL_BACKGROUND=5 + typeset -g POWERLEVEL9K_PER_DIRECTORY_HISTORY_GLOBAL_FOREGROUND=0 + typeset -g POWERLEVEL9K_PER_DIRECTORY_HISTORY_GLOBAL_BACKGROUND=3 + + # Tip: Uncomment the next two lines to hide "local"/"global" text and leave just the icon. + # typeset -g POWERLEVEL9K_PER_DIRECTORY_HISTORY_LOCAL_CONTENT_EXPANSION='' + # typeset -g POWERLEVEL9K_PER_DIRECTORY_HISTORY_GLOBAL_CONTENT_EXPANSION='' + + # Custom icon. + # typeset -g POWERLEVEL9K_PER_DIRECTORY_HISTORY_LOCAL_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_PER_DIRECTORY_HISTORY_GLOBAL_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ################################[ cpu_arch: CPU architecture ]################################ + # CPU architecture color. + typeset -g POWERLEVEL9K_CPU_ARCH_FOREGROUND=0 + typeset -g POWERLEVEL9K_CPU_ARCH_BACKGROUND=3 + + # Hide the segment when on a specific CPU architecture. + # typeset -g POWERLEVEL9K_CPU_ARCH_X86_64_CONTENT_EXPANSION= + # typeset -g POWERLEVEL9K_CPU_ARCH_X86_64_VISUAL_IDENTIFIER_EXPANSION= + + # Custom icon. + # typeset -g POWERLEVEL9K_CPU_ARCH_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##################################[ context: user@hostname ]################################## + # Context color when running with privileges. + typeset -g POWERLEVEL9K_CONTEXT_ROOT_FOREGROUND=1 + typeset -g POWERLEVEL9K_CONTEXT_ROOT_BACKGROUND=0 + # Context color in SSH without privileges. + typeset -g POWERLEVEL9K_CONTEXT_{REMOTE,REMOTE_SUDO}_FOREGROUND=3 + typeset -g POWERLEVEL9K_CONTEXT_{REMOTE,REMOTE_SUDO}_BACKGROUND=0 + # Default context color (no privileges, no SSH). + typeset -g POWERLEVEL9K_CONTEXT_FOREGROUND=3 + typeset -g POWERLEVEL9K_CONTEXT_BACKGROUND=0 + + # Context format when running with privileges: user@hostname. + typeset -g POWERLEVEL9K_CONTEXT_ROOT_TEMPLATE='%n@%m' + # Context format when in SSH without privileges: user@hostname. + typeset -g POWERLEVEL9K_CONTEXT_{REMOTE,REMOTE_SUDO}_TEMPLATE='%n@%m' + # Default context format (no privileges, no SSH): user@hostname. + typeset -g POWERLEVEL9K_CONTEXT_TEMPLATE='%n@%m' + + # Don't show context unless running with privileges or in SSH. + # Tip: Remove the next line to always show context. + typeset -g POWERLEVEL9K_CONTEXT_{DEFAULT,SUDO}_{CONTENT,VISUAL_IDENTIFIER}_EXPANSION= + + # Custom icon. + # typeset -g POWERLEVEL9K_CONTEXT_VISUAL_IDENTIFIER_EXPANSION='⭐' + # Custom prefix. + # typeset -g POWERLEVEL9K_CONTEXT_PREFIX='with ' + + ###[ virtualenv: python virtual environment (https://docs.python.org/3/library/venv.html) ]### + # Python virtual environment color. + typeset -g POWERLEVEL9K_VIRTUALENV_FOREGROUND=0 + typeset -g POWERLEVEL9K_VIRTUALENV_BACKGROUND=4 + # Don't show Python version next to the virtual environment name. + typeset -g POWERLEVEL9K_VIRTUALENV_SHOW_PYTHON_VERSION=false + # If set to "false", won't show virtualenv if pyenv is already shown. + # If set to "if-different", won't show virtualenv if it's the same as pyenv. + typeset -g POWERLEVEL9K_VIRTUALENV_SHOW_WITH_PYENV=false + # Separate environment name from Python version only with a space. + typeset -g POWERLEVEL9K_VIRTUALENV_{LEFT,RIGHT}_DELIMITER= + # Custom icon. + typeset -g POWERLEVEL9K_VIRTUALENV_VISUAL_IDENTIFIER_EXPANSION='🐍' + + #####################[ anaconda: conda environment (https://conda.io/) ]###################### + # Anaconda environment color. + typeset -g POWERLEVEL9K_ANACONDA_FOREGROUND=0 + typeset -g POWERLEVEL9K_ANACONDA_BACKGROUND=4 + + # Anaconda segment format. The following parameters are available within the expansion. + # + # - CONDA_PREFIX Absolute path to the active Anaconda/Miniconda environment. + # - CONDA_DEFAULT_ENV Name of the active Anaconda/Miniconda environment. + # - CONDA_PROMPT_MODIFIER Configurable prompt modifier (see below). + # - P9K_ANACONDA_PYTHON_VERSION Current python version (python --version). + # + # CONDA_PROMPT_MODIFIER can be configured with the following command: + # + # conda config --set env_prompt '({default_env}) ' + # + # The last argument is a Python format string that can use the following variables: + # + # - prefix The same as CONDA_PREFIX. + # - default_env The same as CONDA_DEFAULT_ENV. + # - name The last segment of CONDA_PREFIX. + # - stacked_env Comma-separated list of names in the environment stack. The first element is + # always the same as default_env. + # + # Note: '({default_env}) ' is the default value of env_prompt. + # + # The default value of POWERLEVEL9K_ANACONDA_CONTENT_EXPANSION expands to $CONDA_PROMPT_MODIFIER + # without the surrounding parentheses, or to the last path component of CONDA_PREFIX if the former + # is empty. + typeset -g POWERLEVEL9K_ANACONDA_CONTENT_EXPANSION='${${${${CONDA_PROMPT_MODIFIER#\(}% }%\)}:-${CONDA_PREFIX:t}}' + + # Custom icon. + typeset -g POWERLEVEL9K_ANACONDA_VISUAL_IDENTIFIER_EXPANSION='🐍' + + ################[ pyenv: python environment (https://github.com/pyenv/pyenv) ]################ + # Pyenv color. + typeset -g POWERLEVEL9K_PYENV_FOREGROUND=0 + typeset -g POWERLEVEL9K_PYENV_BACKGROUND=4 + # Hide python version if it doesn't come from one of these sources. + typeset -g POWERLEVEL9K_PYENV_SOURCES=(shell local global) + # If set to false, hide python version if it's the same as global: + # $(pyenv version-name) == $(pyenv global). + typeset -g POWERLEVEL9K_PYENV_PROMPT_ALWAYS_SHOW=false + # If set to false, hide python version if it's equal to "system". + typeset -g POWERLEVEL9K_PYENV_SHOW_SYSTEM=true + + # Pyenv segment format. The following parameters are available within the expansion. + # + # - P9K_CONTENT Current pyenv environment (pyenv version-name). + # - P9K_PYENV_PYTHON_VERSION Current python version (python --version). + # + # The default format has the following logic: + # + # 1. Display just "$P9K_CONTENT" if it's equal to "$P9K_PYENV_PYTHON_VERSION" or + # starts with "$P9K_PYENV_PYTHON_VERSION/". + # 2. Otherwise display "$P9K_CONTENT $P9K_PYENV_PYTHON_VERSION". + typeset -g POWERLEVEL9K_PYENV_CONTENT_EXPANSION='${P9K_CONTENT}${${P9K_CONTENT:#$P9K_PYENV_PYTHON_VERSION(|/*)}:+ $P9K_PYENV_PYTHON_VERSION}' + + # Custom icon. + typeset -g POWERLEVEL9K_PYENV_VISUAL_IDENTIFIER_EXPANSION='🐍' + + ################[ goenv: go environment (https://github.com/syndbg/goenv) ]################ + # Goenv color. + typeset -g POWERLEVEL9K_GOENV_FOREGROUND=0 + typeset -g POWERLEVEL9K_GOENV_BACKGROUND=4 + # Hide go version if it doesn't come from one of these sources. + typeset -g POWERLEVEL9K_GOENV_SOURCES=(shell local global) + # If set to false, hide go version if it's the same as global: + # $(goenv version-name) == $(goenv global). + typeset -g POWERLEVEL9K_GOENV_PROMPT_ALWAYS_SHOW=false + # If set to false, hide go version if it's equal to "system". + typeset -g POWERLEVEL9K_GOENV_SHOW_SYSTEM=true + # Custom icon. + # typeset -g POWERLEVEL9K_GOENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##########[ nodenv: node.js version from nodenv (https://github.com/nodenv/nodenv) ]########## + # Nodenv color. + typeset -g POWERLEVEL9K_NODENV_FOREGROUND=2 + typeset -g POWERLEVEL9K_NODENV_BACKGROUND=0 + # Hide node version if it doesn't come from one of these sources. + typeset -g POWERLEVEL9K_NODENV_SOURCES=(shell local global) + # If set to false, hide node version if it's the same as global: + # $(nodenv version-name) == $(nodenv global). + typeset -g POWERLEVEL9K_NODENV_PROMPT_ALWAYS_SHOW=false + # If set to false, hide node version if it's equal to "system". + typeset -g POWERLEVEL9K_NODENV_SHOW_SYSTEM=true + # Custom icon. + # typeset -g POWERLEVEL9K_NODENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##############[ nvm: node.js version from nvm (https://github.com/nvm-sh/nvm) ]############### + # Nvm color. + typeset -g POWERLEVEL9K_NVM_FOREGROUND=0 + typeset -g POWERLEVEL9K_NVM_BACKGROUND=5 + # If set to false, hide node version if it's the same as default: + # $(nvm version current) == $(nvm version default). + typeset -g POWERLEVEL9K_NVM_PROMPT_ALWAYS_SHOW=false + # If set to false, hide node version if it's equal to "system". + typeset -g POWERLEVEL9K_NVM_SHOW_SYSTEM=true + # Custom icon. + # typeset -g POWERLEVEL9K_NVM_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ############[ nodeenv: node.js environment (https://github.com/ekalinin/nodeenv) ]############ + # Nodeenv color. + typeset -g POWERLEVEL9K_NODEENV_FOREGROUND=2 + typeset -g POWERLEVEL9K_NODEENV_BACKGROUND=0 + # Don't show Node version next to the environment name. + typeset -g POWERLEVEL9K_NODEENV_SHOW_NODE_VERSION=false + # Separate environment name from Node version only with a space. + typeset -g POWERLEVEL9K_NODEENV_{LEFT,RIGHT}_DELIMITER= + # Custom icon. + # typeset -g POWERLEVEL9K_NODEENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##############################[ node_version: node.js version ]############################### + # Node version color. + typeset -g POWERLEVEL9K_NODE_VERSION_FOREGROUND=7 + typeset -g POWERLEVEL9K_NODE_VERSION_BACKGROUND=2 + # Show node version only when in a directory tree containing package.json. + typeset -g POWERLEVEL9K_NODE_VERSION_PROJECT_ONLY=true + # Custom icon. + # typeset -g POWERLEVEL9K_NODE_VERSION_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #######################[ go_version: go version (https://golang.org) ]######################## + # Go version color. + typeset -g POWERLEVEL9K_GO_VERSION_FOREGROUND=255 + typeset -g POWERLEVEL9K_GO_VERSION_BACKGROUND=2 + # Show go version only when in a go project subdirectory. + typeset -g POWERLEVEL9K_GO_VERSION_PROJECT_ONLY=true + # Custom icon. + # typeset -g POWERLEVEL9K_GO_VERSION_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #################[ rust_version: rustc version (https://www.rust-lang.org) ]################## + # Rust version color. + typeset -g POWERLEVEL9K_RUST_VERSION_FOREGROUND=0 + typeset -g POWERLEVEL9K_RUST_VERSION_BACKGROUND=208 + # Show rust version only when in a rust project subdirectory. + typeset -g POWERLEVEL9K_RUST_VERSION_PROJECT_ONLY=true + # Custom icon. + # typeset -g POWERLEVEL9K_RUST_VERSION_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###############[ dotnet_version: .NET version (https://dotnet.microsoft.com) ]################ + # .NET version color. + typeset -g POWERLEVEL9K_DOTNET_VERSION_FOREGROUND=7 + typeset -g POWERLEVEL9K_DOTNET_VERSION_BACKGROUND=5 + # Show .NET version only when in a .NET project subdirectory. + typeset -g POWERLEVEL9K_DOTNET_VERSION_PROJECT_ONLY=true + # Custom icon. + # typeset -g POWERLEVEL9K_DOTNET_VERSION_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #####################[ php_version: php version (https://www.php.net/) ]###################### + # PHP version color. + typeset -g POWERLEVEL9K_PHP_VERSION_FOREGROUND=0 + typeset -g POWERLEVEL9K_PHP_VERSION_BACKGROUND=5 + # Show PHP version only when in a PHP project subdirectory. + typeset -g POWERLEVEL9K_PHP_VERSION_PROJECT_ONLY=true + # Custom icon. + # typeset -g POWERLEVEL9K_PHP_VERSION_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##########[ laravel_version: laravel php framework version (https://laravel.com/) ]########### + # Laravel version color. + typeset -g POWERLEVEL9K_LARAVEL_VERSION_FOREGROUND=1 + typeset -g POWERLEVEL9K_LARAVEL_VERSION_BACKGROUND=7 + # Custom icon. + # typeset -g POWERLEVEL9K_LARAVEL_VERSION_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #############[ rbenv: ruby version from rbenv (https://github.com/rbenv/rbenv) ]############## + # Rbenv color. + typeset -g POWERLEVEL9K_RBENV_FOREGROUND=0 + typeset -g POWERLEVEL9K_RBENV_BACKGROUND=1 + # Hide ruby version if it doesn't come from one of these sources. + typeset -g POWERLEVEL9K_RBENV_SOURCES=(shell local global) + # If set to false, hide ruby version if it's the same as global: + # $(rbenv version-name) == $(rbenv global). + typeset -g POWERLEVEL9K_RBENV_PROMPT_ALWAYS_SHOW=false + # If set to false, hide ruby version if it's equal to "system". + typeset -g POWERLEVEL9K_RBENV_SHOW_SYSTEM=true + # Custom icon. + # typeset -g POWERLEVEL9K_RBENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ####################[ java_version: java version (https://www.java.com/) ]#################### + # Java version color. + typeset -g POWERLEVEL9K_JAVA_VERSION_FOREGROUND=1 + typeset -g POWERLEVEL9K_JAVA_VERSION_BACKGROUND=7 + # Show java version only when in a java project subdirectory. + typeset -g POWERLEVEL9K_JAVA_VERSION_PROJECT_ONLY=true + # Show brief version. + typeset -g POWERLEVEL9K_JAVA_VERSION_FULL=false + # Custom icon. + # typeset -g POWERLEVEL9K_JAVA_VERSION_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###[ package: name@version from package.json (https://docs.npmjs.com/files/package.json) ]#### + # Package color. + typeset -g POWERLEVEL9K_PACKAGE_FOREGROUND=0 + typeset -g POWERLEVEL9K_PACKAGE_BACKGROUND=6 + + # Package format. The following parameters are available within the expansion. + # + # - P9K_PACKAGE_NAME The value of `name` field in package.json. + # - P9K_PACKAGE_VERSION The value of `version` field in package.json. + # + # typeset -g POWERLEVEL9K_PACKAGE_CONTENT_EXPANSION='${P9K_PACKAGE_NAME//\%/%%}@${P9K_PACKAGE_VERSION//\%/%%}' + + # Custom icon. + # typeset -g POWERLEVEL9K_PACKAGE_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #######################[ rvm: ruby version from rvm (https://rvm.io) ]######################## + # Rvm color. + typeset -g POWERLEVEL9K_RVM_FOREGROUND=0 + typeset -g POWERLEVEL9K_RVM_BACKGROUND=240 + # Don't show @gemset at the end. + typeset -g POWERLEVEL9K_RVM_SHOW_GEMSET=false + # Don't show ruby- at the front. + typeset -g POWERLEVEL9K_RVM_SHOW_PREFIX=false + # Custom icon. + # typeset -g POWERLEVEL9K_RVM_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###########[ fvm: flutter version management (https://github.com/leoafarias/fvm) ]############ + # Fvm color. + typeset -g POWERLEVEL9K_FVM_FOREGROUND=0 + typeset -g POWERLEVEL9K_FVM_BACKGROUND=4 + # Custom icon. + # typeset -g POWERLEVEL9K_FVM_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##########[ luaenv: lua version from luaenv (https://github.com/cehoffman/luaenv) ]########### + # Lua color. + typeset -g POWERLEVEL9K_LUAENV_FOREGROUND=0 + typeset -g POWERLEVEL9K_LUAENV_BACKGROUND=4 + # Hide lua version if it doesn't come from one of these sources. + typeset -g POWERLEVEL9K_LUAENV_SOURCES=(shell local global) + # If set to false, hide lua version if it's the same as global: + # $(luaenv version-name) == $(luaenv global). + typeset -g POWERLEVEL9K_LUAENV_PROMPT_ALWAYS_SHOW=false + # If set to false, hide lua version if it's equal to "system". + typeset -g POWERLEVEL9K_LUAENV_SHOW_SYSTEM=true + # Custom icon. + # typeset -g POWERLEVEL9K_LUAENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###############[ jenv: java version from jenv (https://github.com/jenv/jenv) ]################ + # Java color. + typeset -g POWERLEVEL9K_JENV_FOREGROUND=1 + typeset -g POWERLEVEL9K_JENV_BACKGROUND=7 + # Hide java version if it doesn't come from one of these sources. + typeset -g POWERLEVEL9K_JENV_SOURCES=(shell local global) + # If set to false, hide java version if it's the same as global: + # $(jenv version-name) == $(jenv global). + typeset -g POWERLEVEL9K_JENV_PROMPT_ALWAYS_SHOW=false + # If set to false, hide java version if it's equal to "system". + typeset -g POWERLEVEL9K_JENV_SHOW_SYSTEM=true + # Custom icon. + # typeset -g POWERLEVEL9K_JENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###########[ plenv: perl version from plenv (https://github.com/tokuhirom/plenv) ]############ + # Perl color. + typeset -g POWERLEVEL9K_PLENV_FOREGROUND=0 + typeset -g POWERLEVEL9K_PLENV_BACKGROUND=4 + # Hide perl version if it doesn't come from one of these sources. + typeset -g POWERLEVEL9K_PLENV_SOURCES=(shell local global) + # If set to false, hide perl version if it's the same as global: + # $(plenv version-name) == $(plenv global). + typeset -g POWERLEVEL9K_PLENV_PROMPT_ALWAYS_SHOW=false + # If set to false, hide perl version if it's equal to "system". + typeset -g POWERLEVEL9K_PLENV_SHOW_SYSTEM=true + # Custom icon. + # typeset -g POWERLEVEL9K_PLENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###########[ perlbrew: perl version from perlbrew (https://github.com/gugod/App-perlbrew) ]############ + # Perlbrew color. + typeset -g POWERLEVEL9K_PERLBREW_FOREGROUND=67 + # Show perlbrew version only when in a perl project subdirectory. + typeset -g POWERLEVEL9K_PERLBREW_PROJECT_ONLY=true + # Don't show "perl-" at the front. + typeset -g POWERLEVEL9K_PERLBREW_SHOW_PREFIX=false + # Custom icon. + # typeset -g POWERLEVEL9K_PERLBREW_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ############[ phpenv: php version from phpenv (https://github.com/phpenv/phpenv) ]############ + # PHP color. + typeset -g POWERLEVEL9K_PHPENV_FOREGROUND=0 + typeset -g POWERLEVEL9K_PHPENV_BACKGROUND=5 + # Hide php version if it doesn't come from one of these sources. + typeset -g POWERLEVEL9K_PHPENV_SOURCES=(shell local global) + # If set to false, hide php version if it's the same as global: + # $(phpenv version-name) == $(phpenv global). + typeset -g POWERLEVEL9K_PHPENV_PROMPT_ALWAYS_SHOW=false + # If set to false, hide PHP version if it's equal to "system". + typeset -g POWERLEVEL9K_PHPENV_SHOW_SYSTEM=true + # Custom icon. + # typeset -g POWERLEVEL9K_PHPENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #######[ scalaenv: scala version from scalaenv (https://github.com/scalaenv/scalaenv) ]####### + # Scala color. + typeset -g POWERLEVEL9K_SCALAENV_FOREGROUND=0 + typeset -g POWERLEVEL9K_SCALAENV_BACKGROUND=1 + # Hide scala version if it doesn't come from one of these sources. + typeset -g POWERLEVEL9K_SCALAENV_SOURCES=(shell local global) + # If set to false, hide scala version if it's the same as global: + # $(scalaenv version-name) == $(scalaenv global). + typeset -g POWERLEVEL9K_SCALAENV_PROMPT_ALWAYS_SHOW=false + # If set to false, hide scala version if it's equal to "system". + typeset -g POWERLEVEL9K_SCALAENV_SHOW_SYSTEM=true + # Custom icon. + # typeset -g POWERLEVEL9K_SCALAENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##########[ haskell_stack: haskell version from stack (https://haskellstack.org/) ]########### + # Haskell color. + typeset -g POWERLEVEL9K_HASKELL_STACK_FOREGROUND=0 + typeset -g POWERLEVEL9K_HASKELL_STACK_BACKGROUND=3 + + # Hide haskell version if it doesn't come from one of these sources. + # + # shell: version is set by STACK_YAML + # local: version is set by stack.yaml up the directory tree + # global: version is set by the implicit global project (~/.stack/global-project/stack.yaml) + typeset -g POWERLEVEL9K_HASKELL_STACK_SOURCES=(shell local) + # If set to false, hide haskell version if it's the same as in the implicit global project. + typeset -g POWERLEVEL9K_HASKELL_STACK_ALWAYS_SHOW=true + # Custom icon. + # typeset -g POWERLEVEL9K_HASKELL_STACK_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ################[ terraform: terraform workspace (https://www.terraform.io) ]################# + # Don't show terraform workspace if it's literally "default". + typeset -g POWERLEVEL9K_TERRAFORM_SHOW_DEFAULT=false + # POWERLEVEL9K_TERRAFORM_CLASSES is an array with even number of elements. The first element + # in each pair defines a pattern against which the current terraform workspace gets matched. + # More specifically, it's P9K_CONTENT prior to the application of context expansion (see below) + # that gets matched. If you unset all POWERLEVEL9K_TERRAFORM_*CONTENT_EXPANSION parameters, + # you'll see this value in your prompt. The second element of each pair in + # POWERLEVEL9K_TERRAFORM_CLASSES defines the workspace class. Patterns are tried in order. The + # first match wins. + # + # For example, given these settings: + # + # typeset -g POWERLEVEL9K_TERRAFORM_CLASSES=( + # '*prod*' PROD + # '*test*' TEST + # '*' OTHER) + # + # If your current terraform workspace is "project_test", its class is TEST because "project_test" + # doesn't match the pattern '*prod*' but does match '*test*'. + # + # You can define different colors, icons and content expansions for different classes: + # + # typeset -g POWERLEVEL9K_TERRAFORM_TEST_FOREGROUND=2 + # typeset -g POWERLEVEL9K_TERRAFORM_TEST_BACKGROUND=0 + # typeset -g POWERLEVEL9K_TERRAFORM_TEST_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_TERRAFORM_TEST_CONTENT_EXPANSION='> ${P9K_CONTENT} <' + typeset -g POWERLEVEL9K_TERRAFORM_CLASSES=( + # '*prod*' PROD # These values are examples that are unlikely + # '*test*' TEST # to match your needs. Customize them as needed. + '*' OTHER) + typeset -g POWERLEVEL9K_TERRAFORM_OTHER_FOREGROUND=4 + typeset -g POWERLEVEL9K_TERRAFORM_OTHER_BACKGROUND=0 + # typeset -g POWERLEVEL9K_TERRAFORM_OTHER_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #############[ terraform_version: terraform version (https://www.terraform.io) ]############## + # Terraform version color. + typeset -g POWERLEVEL9K_TERRAFORM_VERSION_FOREGROUND=4 + typeset -g POWERLEVEL9K_TERRAFORM_VERSION_BACKGROUND=0 + # Custom icon. + # typeset -g POWERLEVEL9K_TERRAFORM_VERSION_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #############[ kubecontext: current kubernetes context (https://kubernetes.io/) ]############# + # Show kubecontext only when the command you are typing invokes one of these tools. + # Tip: Remove the next line to always show kubecontext. + typeset -g POWERLEVEL9K_KUBECONTEXT_SHOW_ON_COMMAND='kubectl|helm|kubens|kubectx|oc|istioctl|kogito|k9s|helmfile|flux|fluxctl|stern|kubeseal|skaffold|kubent|kubecolor|cmctl|sparkctl' + + # Kubernetes context classes for the purpose of using different colors, icons and expansions with + # different contexts. + # + # POWERLEVEL9K_KUBECONTEXT_CLASSES is an array with even number of elements. The first element + # in each pair defines a pattern against which the current kubernetes context gets matched. + # More specifically, it's P9K_CONTENT prior to the application of context expansion (see below) + # that gets matched. If you unset all POWERLEVEL9K_KUBECONTEXT_*CONTENT_EXPANSION parameters, + # you'll see this value in your prompt. The second element of each pair in + # POWERLEVEL9K_KUBECONTEXT_CLASSES defines the context class. Patterns are tried in order. The + # first match wins. + # + # For example, given these settings: + # + # typeset -g POWERLEVEL9K_KUBECONTEXT_CLASSES=( + # '*prod*' PROD + # '*test*' TEST + # '*' DEFAULT) + # + # If your current kubernetes context is "deathray-testing/default", its class is TEST + # because "deathray-testing/default" doesn't match the pattern '*prod*' but does match '*test*'. + # + # You can define different colors, icons and content expansions for different classes: + # + # typeset -g POWERLEVEL9K_KUBECONTEXT_TEST_FOREGROUND=0 + # typeset -g POWERLEVEL9K_KUBECONTEXT_TEST_BACKGROUND=2 + # typeset -g POWERLEVEL9K_KUBECONTEXT_TEST_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_KUBECONTEXT_TEST_CONTENT_EXPANSION='> ${P9K_CONTENT} <' + typeset -g POWERLEVEL9K_KUBECONTEXT_CLASSES=( + # '*prod*' PROD # These values are examples that are unlikely + # '*test*' TEST # to match your needs. Customize them as needed. + '*' DEFAULT) + typeset -g POWERLEVEL9K_KUBECONTEXT_DEFAULT_FOREGROUND=7 + typeset -g POWERLEVEL9K_KUBECONTEXT_DEFAULT_BACKGROUND=5 + # typeset -g POWERLEVEL9K_KUBECONTEXT_DEFAULT_VISUAL_IDENTIFIER_EXPANSION='⭐' + + # Use POWERLEVEL9K_KUBECONTEXT_CONTENT_EXPANSION to specify the content displayed by kubecontext + # segment. Parameter expansions are very flexible and fast, too. See reference: + # http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion. + # + # Within the expansion the following parameters are always available: + # + # - P9K_CONTENT The content that would've been displayed if there was no content + # expansion defined. + # - P9K_KUBECONTEXT_NAME The current context's name. Corresponds to column NAME in the + # output of `kubectl config get-contexts`. + # - P9K_KUBECONTEXT_CLUSTER The current context's cluster. Corresponds to column CLUSTER in the + # output of `kubectl config get-contexts`. + # - P9K_KUBECONTEXT_NAMESPACE The current context's namespace. Corresponds to column NAMESPACE + # in the output of `kubectl config get-contexts`. If there is no + # namespace, the parameter is set to "default". + # - P9K_KUBECONTEXT_USER The current context's user. Corresponds to column AUTHINFO in the + # output of `kubectl config get-contexts`. + # + # If the context points to Google Kubernetes Engine (GKE) or Elastic Kubernetes Service (EKS), + # the following extra parameters are available: + # + # - P9K_KUBECONTEXT_CLOUD_NAME Either "gke" or "eks". + # - P9K_KUBECONTEXT_CLOUD_ACCOUNT Account/project ID. + # - P9K_KUBECONTEXT_CLOUD_ZONE Availability zone. + # - P9K_KUBECONTEXT_CLOUD_CLUSTER Cluster. + # + # P9K_KUBECONTEXT_CLOUD_* parameters are derived from P9K_KUBECONTEXT_CLUSTER. For example, + # if P9K_KUBECONTEXT_CLUSTER is "gke_my-account_us-east1-a_my-cluster-01": + # + # - P9K_KUBECONTEXT_CLOUD_NAME=gke + # - P9K_KUBECONTEXT_CLOUD_ACCOUNT=my-account + # - P9K_KUBECONTEXT_CLOUD_ZONE=us-east1-a + # - P9K_KUBECONTEXT_CLOUD_CLUSTER=my-cluster-01 + # + # If P9K_KUBECONTEXT_CLUSTER is "arn:aws:eks:us-east-1:123456789012:cluster/my-cluster-01": + # + # - P9K_KUBECONTEXT_CLOUD_NAME=eks + # - P9K_KUBECONTEXT_CLOUD_ACCOUNT=123456789012 + # - P9K_KUBECONTEXT_CLOUD_ZONE=us-east-1 + # - P9K_KUBECONTEXT_CLOUD_CLUSTER=my-cluster-01 + typeset -g POWERLEVEL9K_KUBECONTEXT_DEFAULT_CONTENT_EXPANSION= + # Show P9K_KUBECONTEXT_CLOUD_CLUSTER if it's not empty and fall back to P9K_KUBECONTEXT_NAME. + POWERLEVEL9K_KUBECONTEXT_DEFAULT_CONTENT_EXPANSION+='${P9K_KUBECONTEXT_CLOUD_CLUSTER:-${P9K_KUBECONTEXT_NAME}}' + # Append the current context's namespace if it's not "default". + POWERLEVEL9K_KUBECONTEXT_DEFAULT_CONTENT_EXPANSION+='${${:-/$P9K_KUBECONTEXT_NAMESPACE}:#/default}' + + # Custom prefix. + # typeset -g POWERLEVEL9K_KUBECONTEXT_PREFIX='at ' + + #[ aws: aws profile (https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html) ]# + # Show aws only when the command you are typing invokes one of these tools. + # Tip: Remove the next line to always show aws. + typeset -g POWERLEVEL9K_AWS_SHOW_ON_COMMAND='aws|awless|cdk|terraform|tofu|pulumi|terragrunt' + + # POWERLEVEL9K_AWS_CLASSES is an array with even number of elements. The first element + # in each pair defines a pattern against which the current AWS profile gets matched. + # More specifically, it's P9K_CONTENT prior to the application of context expansion (see below) + # that gets matched. If you unset all POWERLEVEL9K_AWS_*CONTENT_EXPANSION parameters, + # you'll see this value in your prompt. The second element of each pair in + # POWERLEVEL9K_AWS_CLASSES defines the profile class. Patterns are tried in order. The + # first match wins. + # + # For example, given these settings: + # + # typeset -g POWERLEVEL9K_AWS_CLASSES=( + # '*prod*' PROD + # '*test*' TEST + # '*' DEFAULT) + # + # If your current AWS profile is "company_test", its class is TEST + # because "company_test" doesn't match the pattern '*prod*' but does match '*test*'. + # + # You can define different colors, icons and content expansions for different classes: + # + # typeset -g POWERLEVEL9K_AWS_TEST_FOREGROUND=28 + # typeset -g POWERLEVEL9K_AWS_TEST_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_AWS_TEST_CONTENT_EXPANSION='> ${P9K_CONTENT} <' + typeset -g POWERLEVEL9K_AWS_CLASSES=( + # '*prod*' PROD # These values are examples that are unlikely + # '*test*' TEST # to match your needs. Customize them as needed. + '*' DEFAULT) + typeset -g POWERLEVEL9K_AWS_DEFAULT_FOREGROUND=7 + typeset -g POWERLEVEL9K_AWS_DEFAULT_BACKGROUND=1 + # typeset -g POWERLEVEL9K_AWS_DEFAULT_VISUAL_IDENTIFIER_EXPANSION='⭐' + + # AWS segment format. The following parameters are available within the expansion. + # + # - P9K_AWS_PROFILE The name of the current AWS profile. + # - P9K_AWS_REGION The region associated with the current AWS profile. + typeset -g POWERLEVEL9K_AWS_CONTENT_EXPANSION='${P9K_AWS_PROFILE//\%/%%}${P9K_AWS_REGION:+ ${P9K_AWS_REGION//\%/%%}}' + + #[ aws_eb_env: aws elastic beanstalk environment (https://aws.amazon.com/elasticbeanstalk/) ]# + # AWS Elastic Beanstalk environment color. + typeset -g POWERLEVEL9K_AWS_EB_ENV_FOREGROUND=2 + typeset -g POWERLEVEL9K_AWS_EB_ENV_BACKGROUND=0 + # Custom icon. + # typeset -g POWERLEVEL9K_AWS_EB_ENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##########[ azure: azure account name (https://docs.microsoft.com/en-us/cli/azure) ]########## + # Show azure only when the command you are typing invokes one of these tools. + # Tip: Remove the next line to always show azure. + typeset -g POWERLEVEL9K_AZURE_SHOW_ON_COMMAND='az|terraform|tofu|pulumi|terragrunt' + + # POWERLEVEL9K_AZURE_CLASSES is an array with even number of elements. The first element + # in each pair defines a pattern against which the current azure account name gets matched. + # More specifically, it's P9K_CONTENT prior to the application of context expansion (see below) + # that gets matched. If you unset all POWERLEVEL9K_AZURE_*CONTENT_EXPANSION parameters, + # you'll see this value in your prompt. The second element of each pair in + # POWERLEVEL9K_AZURE_CLASSES defines the account class. Patterns are tried in order. The + # first match wins. + # + # For example, given these settings: + # + # typeset -g POWERLEVEL9K_AZURE_CLASSES=( + # '*prod*' PROD + # '*test*' TEST + # '*' OTHER) + # + # If your current azure account is "company_test", its class is TEST because "company_test" + # doesn't match the pattern '*prod*' but does match '*test*'. + # + # You can define different colors, icons and content expansions for different classes: + # + # typeset -g POWERLEVEL9K_AZURE_TEST_FOREGROUND=2 + # typeset -g POWERLEVEL9K_AZURE_TEST_BACKGROUND=0 + # typeset -g POWERLEVEL9K_AZURE_TEST_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_AZURE_TEST_CONTENT_EXPANSION='> ${P9K_CONTENT} <' + typeset -g POWERLEVEL9K_AZURE_CLASSES=( + # '*prod*' PROD # These values are examples that are unlikely + # '*test*' TEST # to match your needs. Customize them as needed. + '*' OTHER) + + # Azure account name color. + typeset -g POWERLEVEL9K_AZURE_OTHER_FOREGROUND=7 + typeset -g POWERLEVEL9K_AZURE_OTHER_BACKGROUND=4 + # Custom icon. + # typeset -g POWERLEVEL9K_AZURE_OTHER_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##########[ gcloud: google cloud account and project (https://cloud.google.com/) ]########### + # Show gcloud only when the command you are typing invokes one of these tools. + # Tip: Remove the next line to always show gcloud. + typeset -g POWERLEVEL9K_GCLOUD_SHOW_ON_COMMAND='gcloud|gcs|gsutil' + # Google cloud color. + typeset -g POWERLEVEL9K_GCLOUD_FOREGROUND=7 + typeset -g POWERLEVEL9K_GCLOUD_BACKGROUND=4 + + # Google cloud format. Change the value of POWERLEVEL9K_GCLOUD_PARTIAL_CONTENT_EXPANSION and/or + # POWERLEVEL9K_GCLOUD_COMPLETE_CONTENT_EXPANSION if the default is too verbose or not informative + # enough. You can use the following parameters in the expansions. Each of them corresponds to the + # output of `gcloud` tool. + # + # Parameter | Source + # -------------------------|-------------------------------------------------------------------- + # P9K_GCLOUD_CONFIGURATION | gcloud config configurations list --format='value(name)' + # P9K_GCLOUD_ACCOUNT | gcloud config get-value account + # P9K_GCLOUD_PROJECT_ID | gcloud config get-value project + # P9K_GCLOUD_PROJECT_NAME | gcloud projects describe $P9K_GCLOUD_PROJECT_ID --format='value(name)' + # + # Note: ${VARIABLE//\%/%%} expands to ${VARIABLE} with all occurrences of '%' replaced with '%%'. + # + # Obtaining project name requires sending a request to Google servers. This can take a long time + # and even fail. When project name is unknown, P9K_GCLOUD_PROJECT_NAME is not set and gcloud + # prompt segment is in state PARTIAL. When project name gets known, P9K_GCLOUD_PROJECT_NAME gets + # set and gcloud prompt segment transitions to state COMPLETE. + # + # You can customize the format, icon and colors of gcloud segment separately for states PARTIAL + # and COMPLETE. You can also hide gcloud in state PARTIAL by setting + # POWERLEVEL9K_GCLOUD_PARTIAL_VISUAL_IDENTIFIER_EXPANSION and + # POWERLEVEL9K_GCLOUD_PARTIAL_CONTENT_EXPANSION to empty. + typeset -g POWERLEVEL9K_GCLOUD_PARTIAL_CONTENT_EXPANSION='${P9K_GCLOUD_PROJECT_ID//\%/%%}' + typeset -g POWERLEVEL9K_GCLOUD_COMPLETE_CONTENT_EXPANSION='${P9K_GCLOUD_PROJECT_NAME//\%/%%}' + + # Send a request to Google (by means of `gcloud projects describe ...`) to obtain project name + # this often. Negative value disables periodic polling. In this mode project name is retrieved + # only when the current configuration, account or project id changes. + typeset -g POWERLEVEL9K_GCLOUD_REFRESH_PROJECT_NAME_SECONDS=60 + + # Custom icon. + # typeset -g POWERLEVEL9K_GCLOUD_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #[ google_app_cred: google application credentials (https://cloud.google.com/docs/authentication/production) ]# + # Show google_app_cred only when the command you are typing invokes one of these tools. + # Tip: Remove the next line to always show google_app_cred. + typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_SHOW_ON_COMMAND='terraform|tofu|pulumi|terragrunt' + + # Google application credentials classes for the purpose of using different colors, icons and + # expansions with different credentials. + # + # POWERLEVEL9K_GOOGLE_APP_CRED_CLASSES is an array with even number of elements. The first + # element in each pair defines a pattern against which the current kubernetes context gets + # matched. More specifically, it's P9K_CONTENT prior to the application of context expansion + # (see below) that gets matched. If you unset all POWERLEVEL9K_GOOGLE_APP_CRED_*CONTENT_EXPANSION + # parameters, you'll see this value in your prompt. The second element of each pair in + # POWERLEVEL9K_GOOGLE_APP_CRED_CLASSES defines the context class. Patterns are tried in order. + # The first match wins. + # + # For example, given these settings: + # + # typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_CLASSES=( + # '*:*prod*:*' PROD + # '*:*test*:*' TEST + # '*' DEFAULT) + # + # If your current Google application credentials is "service_account deathray-testing x@y.com", + # its class is TEST because it doesn't match the pattern '* *prod* *' but does match '* *test* *'. + # + # You can define different colors, icons and content expansions for different classes: + # + # typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_TEST_FOREGROUND=28 + # typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_TEST_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_TEST_CONTENT_EXPANSION='$P9K_GOOGLE_APP_CRED_PROJECT_ID' + typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_CLASSES=( + # '*:*prod*:*' PROD # These values are examples that are unlikely + # '*:*test*:*' TEST # to match your needs. Customize them as needed. + '*' DEFAULT) + typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_DEFAULT_FOREGROUND=7 + typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_DEFAULT_BACKGROUND=4 + # typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_DEFAULT_VISUAL_IDENTIFIER_EXPANSION='⭐' + + # Use POWERLEVEL9K_GOOGLE_APP_CRED_CONTENT_EXPANSION to specify the content displayed by + # google_app_cred segment. Parameter expansions are very flexible and fast, too. See reference: + # http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion. + # + # You can use the following parameters in the expansion. Each of them corresponds to one of the + # fields in the JSON file pointed to by GOOGLE_APPLICATION_CREDENTIALS. + # + # Parameter | JSON key file field + # ---------------------------------+--------------- + # P9K_GOOGLE_APP_CRED_TYPE | type + # P9K_GOOGLE_APP_CRED_PROJECT_ID | project_id + # P9K_GOOGLE_APP_CRED_CLIENT_EMAIL | client_email + # + # Note: ${VARIABLE//\%/%%} expands to ${VARIABLE} with all occurrences of '%' replaced by '%%'. + typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_DEFAULT_CONTENT_EXPANSION='${P9K_GOOGLE_APP_CRED_PROJECT_ID//\%/%%}' + + ##############[ toolbox: toolbox name (https://github.com/containers/toolbox) ]############### + # Toolbox color. + typeset -g POWERLEVEL9K_TOOLBOX_FOREGROUND=0 + typeset -g POWERLEVEL9K_TOOLBOX_BACKGROUND=3 + # Don't display the name of the toolbox if it matches fedora-toolbox-*. + typeset -g POWERLEVEL9K_TOOLBOX_CONTENT_EXPANSION='${P9K_TOOLBOX_NAME:#fedora-toolbox-*}' + # Custom icon. + # typeset -g POWERLEVEL9K_TOOLBOX_VISUAL_IDENTIFIER_EXPANSION='⭐' + # Custom prefix. + # typeset -g POWERLEVEL9K_TOOLBOX_PREFIX='in ' + + ###############################[ public_ip: public IP address ]############################### + # Public IP color. + typeset -g POWERLEVEL9K_PUBLIC_IP_FOREGROUND=7 + typeset -g POWERLEVEL9K_PUBLIC_IP_BACKGROUND=0 + # Custom icon. + # typeset -g POWERLEVEL9K_PUBLIC_IP_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ########################[ vpn_ip: virtual private network indicator ]######################### + # VPN IP color. + typeset -g POWERLEVEL9K_VPN_IP_FOREGROUND=0 + typeset -g POWERLEVEL9K_VPN_IP_BACKGROUND=6 + # When on VPN, show just an icon without the IP address. + # Tip: To display the private IP address when on VPN, remove the next line. + typeset -g POWERLEVEL9K_VPN_IP_CONTENT_EXPANSION= + # Regular expression for the VPN network interface. Run `ifconfig` or `ip -4 a show` while on VPN + # to see the name of the interface. + typeset -g POWERLEVEL9K_VPN_IP_INTERFACE='(gpd|wg|(.*tun)|tailscale)[0-9]*|(zt.*)' + # If set to true, show one segment per matching network interface. If set to false, show only + # one segment corresponding to the first matching network interface. + # Tip: If you set it to true, you'll probably want to unset POWERLEVEL9K_VPN_IP_CONTENT_EXPANSION. + typeset -g POWERLEVEL9K_VPN_IP_SHOW_ALL=false + # Custom icon. + # typeset -g POWERLEVEL9K_VPN_IP_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###########[ ip: ip address and bandwidth usage for a specified network interface ]########### + # IP color. + typeset -g POWERLEVEL9K_IP_BACKGROUND=4 + typeset -g POWERLEVEL9K_IP_FOREGROUND=0 + # The following parameters are accessible within the expansion: + # + # Parameter | Meaning + # ----------------------+------------------------------------------- + # P9K_IP_IP | IP address + # P9K_IP_INTERFACE | network interface + # P9K_IP_RX_BYTES | total number of bytes received + # P9K_IP_TX_BYTES | total number of bytes sent + # P9K_IP_RX_BYTES_DELTA | number of bytes received since last prompt + # P9K_IP_TX_BYTES_DELTA | number of bytes sent since last prompt + # P9K_IP_RX_RATE | receive rate (since last prompt) + # P9K_IP_TX_RATE | send rate (since last prompt) + typeset -g POWERLEVEL9K_IP_CONTENT_EXPANSION='${P9K_IP_RX_RATE:+⇣$P9K_IP_RX_RATE }${P9K_IP_TX_RATE:+⇡$P9K_IP_TX_RATE }$P9K_IP_IP' + # Show information for the first network interface whose name matches this regular expression. + # Run `ifconfig` or `ip -4 a show` to see the names of all network interfaces. + typeset -g POWERLEVEL9K_IP_INTERFACE='[ew].*' + # Custom icon. + # typeset -g POWERLEVEL9K_IP_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #########################[ proxy: system-wide http/https/ftp proxy ]########################## + # Proxy color. + typeset -g POWERLEVEL9K_PROXY_FOREGROUND=4 + typeset -g POWERLEVEL9K_PROXY_BACKGROUND=0 + # Custom icon. + # typeset -g POWERLEVEL9K_PROXY_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ################################[ battery: internal battery ]################################# + # Show battery in red when it's below this level and not connected to power supply. + typeset -g POWERLEVEL9K_BATTERY_LOW_THRESHOLD=20 + typeset -g POWERLEVEL9K_BATTERY_LOW_FOREGROUND=1 + # Show battery in green when it's charging or fully charged. + typeset -g POWERLEVEL9K_BATTERY_{CHARGING,CHARGED}_FOREGROUND=2 + # Show battery in yellow when it's discharging. + typeset -g POWERLEVEL9K_BATTERY_DISCONNECTED_FOREGROUND=3 + # Battery pictograms going from low to high level of charge. + typeset -g POWERLEVEL9K_BATTERY_STAGES=('%K{232}▁' '%K{232}▂' '%K{232}▃' '%K{232}▄' '%K{232}▅' '%K{232}▆' '%K{232}▇' '%K{232}█') + # Don't show the remaining time to charge/discharge. + typeset -g POWERLEVEL9K_BATTERY_VERBOSE=false + typeset -g POWERLEVEL9K_BATTERY_BACKGROUND=0 + + #####################################[ wifi: wifi speed ]##################################### + # WiFi color. + typeset -g POWERLEVEL9K_WIFI_FOREGROUND=0 + typeset -g POWERLEVEL9K_WIFI_BACKGROUND=4 + # Custom icon. + # typeset -g POWERLEVEL9K_WIFI_VISUAL_IDENTIFIER_EXPANSION='⭐' + + # Use different colors and icons depending on signal strength ($P9K_WIFI_BARS). + # + # # Wifi colors and icons for different signal strength levels (low to high). + # typeset -g my_wifi_fg=(0 0 0 0 0) # <-- change these values + # typeset -g my_wifi_icon=('WiFi' 'WiFi' 'WiFi' 'WiFi' 'WiFi') # <-- change these values + # + # typeset -g POWERLEVEL9K_WIFI_CONTENT_EXPANSION='%F{${my_wifi_fg[P9K_WIFI_BARS+1]}}$P9K_WIFI_LAST_TX_RATE Mbps' + # typeset -g POWERLEVEL9K_WIFI_VISUAL_IDENTIFIER_EXPANSION='%F{${my_wifi_fg[P9K_WIFI_BARS+1]}}${my_wifi_icon[P9K_WIFI_BARS+1]}' + # + # The following parameters are accessible within the expansions: + # + # Parameter | Meaning + # ----------------------+--------------- + # P9K_WIFI_SSID | service set identifier, a.k.a. network name + # P9K_WIFI_LINK_AUTH | authentication protocol such as "wpa2-psk" or "none"; empty if unknown + # P9K_WIFI_LAST_TX_RATE | wireless transmit rate in megabits per second + # P9K_WIFI_RSSI | signal strength in dBm, from -120 to 0 + # P9K_WIFI_NOISE | noise in dBm, from -120 to 0 + # P9K_WIFI_BARS | signal strength in bars, from 0 to 4 (derived from P9K_WIFI_RSSI and P9K_WIFI_NOISE) + + ####################################[ time: current time ]#################################### + # Current time color. + typeset -g POWERLEVEL9K_TIME_FOREGROUND=0 + typeset -g POWERLEVEL9K_TIME_BACKGROUND=7 + # Format for the current time: 09:51:02. See `man 3 strftime`. + typeset -g POWERLEVEL9K_TIME_FORMAT='%D{%H:%M:%S}' + # If set to true, time will update when you hit enter. This way prompts for the past + # commands will contain the start times of their commands as opposed to the default + # behavior where they contain the end times of their preceding commands. + typeset -g POWERLEVEL9K_TIME_UPDATE_ON_COMMAND=false + # Custom icon. + # typeset -g POWERLEVEL9K_TIME_VISUAL_IDENTIFIER_EXPANSION='⭐' + # Custom prefix. + # typeset -g POWERLEVEL9K_TIME_PREFIX='at ' + + # Example of a user-defined prompt segment. Function prompt_example will be called on every + # prompt if `example` prompt segment is added to POWERLEVEL9K_LEFT_PROMPT_ELEMENTS or + # POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS. It displays an icon and yellow text on red background + # greeting the user. + # + # Type `p10k help segment` for documentation and a more sophisticated example. + function prompt_example() { + p10k segment -b 1 -f 3 -i '⭐' -t 'hello, %n' + } + + # User-defined prompt segments may optionally provide an instant_prompt_* function. Its job + # is to generate the prompt segment for display in instant prompt. See + # https://github.com/romkatv/powerlevel10k#instant-prompt. + # + # Powerlevel10k will call instant_prompt_* at the same time as the regular prompt_* function + # and will record all `p10k segment` calls it makes. When displaying instant prompt, Powerlevel10k + # will replay these calls without actually calling instant_prompt_*. It is imperative that + # instant_prompt_* always makes the same `p10k segment` calls regardless of environment. If this + # rule is not observed, the content of instant prompt will be incorrect. + # + # Usually, you should either not define instant_prompt_* or simply call prompt_* from it. If + # instant_prompt_* is not defined for a segment, the segment won't be shown in instant prompt. + function instant_prompt_example() { + # Since prompt_example always makes the same `p10k segment` calls, we can call it from + # instant_prompt_example. This will give us the same `example` prompt segment in the instant + # and regular prompts. + prompt_example + } + + # User-defined prompt segments can be customized the same way as built-in segments. + typeset -g POWERLEVEL9K_EXAMPLE_FOREGROUND=3 + typeset -g POWERLEVEL9K_EXAMPLE_BACKGROUND=1 + # typeset -g POWERLEVEL9K_EXAMPLE_VISUAL_IDENTIFIER_EXPANSION='⭐' + + # Transient prompt works similarly to the builtin transient_rprompt option. It trims down prompt + # when accepting a command line. Supported values: + # + # - off: Don't change prompt when accepting a command line. + # - always: Trim down prompt when accepting a command line. + # - same-dir: Trim down prompt when accepting a command line unless this is the first command + # typed after changing current working directory. + typeset -g POWERLEVEL9K_TRANSIENT_PROMPT=always + + # Instant prompt mode. + # + # - off: Disable instant prompt. Choose this if you've tried instant prompt and found + # it incompatible with your zsh configuration files. + # - quiet: Enable instant prompt and don't print warnings when detecting console output + # during zsh initialization. Choose this if you've read and understood + # https://github.com/romkatv/powerlevel10k#instant-prompt. + # - verbose: Enable instant prompt and print a warning when detecting console output during + # zsh initialization. Choose this if you've never tried instant prompt, haven't + # seen the warning, or if you are unsure what this all means. + typeset -g POWERLEVEL9K_INSTANT_PROMPT=verbose + + # Hot reload allows you to change POWERLEVEL9K options after Powerlevel10k has been initialized. + # For example, you can type POWERLEVEL9K_BACKGROUND=red and see your prompt turn red. Hot reload + # can slow down prompt by 1-2 milliseconds, so it's better to keep it turned off unless you + # really need it. + typeset -g POWERLEVEL9K_DISABLE_HOT_RELOAD=true + + # If p10k is already loaded, reload configuration. + # This works even with POWERLEVEL9K_DISABLE_HOT_RELOAD=true. + (( ! $+functions[p10k] )) || p10k reload +} + +# Tell `p10k configure` which file it should overwrite. +typeset -g POWERLEVEL9K_CONFIG_FILE=${${(%):-%x}:a} + +(( ${#p10k_config_opts} )) && setopt ${p10k_config_opts[@]} +'builtin' 'unset' 'p10k_config_opts' diff --git a/securecheck/assets/securecheck-icon.ico b/securecheck/assets/securecheck-icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..20c92d189d3e3a86e1e3ecc1f78ae82e732616be GIT binary patch literal 17897 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 x^Jy!0fxdBT$WD^+(f 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 Vosed?0v^`F;OXk;vd$@?2>{z*LOcKf literal 0 HcmV?d00001 diff --git a/securecheck/assets/securecheck-icon.png b/securecheck/assets/securecheck-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..4543486a279726128b0ce1cec4cd417400b6d485 GIT binary patch literal 17894 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 Xd)ilhE7vLD87&N+u6{1-oD!M + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/securecheck/catalog.py b/securecheck/catalog.py new file mode 100644 index 0000000..5b1254b --- /dev/null +++ b/securecheck/catalog.py @@ -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, + ), + ] diff --git a/securecheck/config.py b/securecheck/config.py new file mode 100644 index 0000000..fae8267 --- /dev/null +++ b/securecheck/config.py @@ -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") diff --git a/securecheck/executor.py b/securecheck/executor.py new file mode 100644 index 0000000..5d54785 --- /dev/null +++ b/securecheck/executor.py @@ -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 diff --git a/securecheck/logging_utils.py b/securecheck/logging_utils.py new file mode 100644 index 0000000..f08d685 --- /dev/null +++ b/securecheck/logging_utils.py @@ -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 diff --git a/securecheck/models.py b/securecheck/models.py new file mode 100644 index 0000000..b5b2104 --- /dev/null +++ b/securecheck/models.py @@ -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 diff --git a/securecheck/status.py b/securecheck/status.py new file mode 100644 index 0000000..e43a658 --- /dev/null +++ b/securecheck/status.py @@ -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 diff --git a/securecheck/storage.py b/securecheck/storage.py new file mode 100644 index 0000000..4c61bdf --- /dev/null +++ b/securecheck/storage.py @@ -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 diff --git a/securecheck/system_info.py b/securecheck/system_info.py new file mode 100644 index 0000000..52df69d --- /dev/null +++ b/securecheck/system_info.py @@ -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, + ) diff --git a/securecheck/tasks.py b/securecheck/tasks.py new file mode 100644 index 0000000..7a6fd56 --- /dev/null +++ b/securecheck/tasks.py @@ -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), + )