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