mnemo-rs 1.9.3

Local-first shell history manager with CLI/TUI, fuzzy search, project/session context, and DevSecOps-focused release hardening.
Documentation
# Threat model de mnemo

Modèle de menaces de mnemo, outil **local-first** de navigation dans
l'historique shell. L'analyse suit une approche actifs → menaces → mitigations →
risques résiduels, adaptée à un outil mono-utilisateur sans composant serveur.

## 1. Périmètre et hypothèses

- mnemo s'exécute **localement**, sous le compte d'un utilisateur Linux.
- Pas de service réseau exposé, pas de multi-tenant, pas de privilèges root
  requis.
- Le seul trafic sortant provient de `update` / `upgrade` vers GitHub (HTTPS).
- L'attaquant pertinent est : une **archive malveillante** fournie à `restore`,
  un **asset de release altéré**, ou une **erreur de manipulation** de
  l'utilisateur entraînant une perte de données.

## 2. Actifs protégés

| Actif | Emplacement | Sensibilité |
| --- | --- | --- |
| `history.db` | `~/.local/share/mnemo/history.db` | Élevée (historique de commandes) |
| `config.toml` | `~/.config/mnemo/config.toml` | Moyenne (préférences, mots-clés sensibles) |
| Sauvegardes | `~/.local/share/mnemo/backups/` | Élevée (copies de la base) |
| `.bashrc` | `~/.bashrc` | Élevée (exécuté à chaque shell) |
| Binaire `mnemo` | `~/.local/bin/mnemo` | Élevée (code exécuté) |

## 3. Menaces, mitigations et risques résiduels

### M1 : Perte accidentelle de données
- **Menace** : suppression involontaire de la base / config via `delete`,
  `prune`, `maintenance run`, `restore` ou `uninstall`.
- **Mitigations** : `--dry-run` partout ; confirmation obligatoire (refus en
  non-interactif sans `--yes`) ; **sauvegarde automatique** avant toute action
  destructive ; `uninstall` conserve les données par défaut. Le nettoyage
  automatique (`maintenance run`) est **désactivé par défaut**
  (`auto_prune_enabled = false`), exige `--yes`, et crée une sauvegarde avant
  purge si `auto_backup_before_prune = true`.
- **Risque résiduel** : un utilisateur passant `--yes --purge` en connaissance
  de cause supprime ses données (une sauvegarde de sécurité reste créée avant).

### M2 : Archive de restauration malveillante
- **Menace** : `mnemo restore archive.tar.gz` où l'archive est forgée.
- **Mitigations** : extraction dans un **dossier temporaire** isolé ;
  validation de chaque entrée (cf. M3) ; ouverture de la base en **lecture
  seule** + validation (table `commands`, version de schéma) **avant**
  remplacement ; sauvegarde de sécurité préalable.
- **Risque résiduel** : une base SQLite valide mais au contenu trompeur peut
  être restaurée, mais elle ne s'exécute pas (données, pas du code).

### M3 : Path traversal dans une archive tar
- **Menace** : entrées `../evil`, `/etc/cron.d/x`, liens symboliques sortant de
  la racine d'extraction.
- **Mitigations** : `src/archive.rs::safe_unpack` valide **chaque** entrée
  (rejet des chemins absolus, des `..` et des cibles de liens hors racine)
  **avant** écriture ; `unpack_in` de la crate `tar` revalide (défense en
  profondeur) ; extraction limitée au tempdir.
- **Risque résiduel** : négligeable ; couvert par tests unitaires et
  d'intégration (`restore`, `upgrade`).

### M4 : Mise à niveau corrompue / downgrade
- **Menace** : asset de release tronqué, altéré, ou substitué.
- **Mitigations** : vérification **SHA-256** de l'archive **avant** extraction
  (toujours obligatoire et bloquante) ; **vérification de la signature Sigstore**
  de l'archive (`cosign verify-blob`) lorsque `cosign` est présent, en défense en
  profondeur ; HTTPS obligatoire ; le nouveau binaire est testé (`--version`)
  avant remplacement **atomique** ; sauvegarde des données avant bascule ; en cas
  d'échec, le binaire en place reste intact.
- **Modes de vérification de signature (v0.8)** :
  - *best-effort* (défaut) : si `cosign` est absent ou si le bundle est
    indisponible, l'opération continue après avertissement, le SHA-256 étant déjà
    garanti. Une signature **invalide** refuse toujours l'installation.
  - *strict* (`mnemo upgrade --require-signature`, `MNEMO_REQUIRE_SIGNATURE=1`
    pour `install.sh`) : l'installation est refusée si la signature ne peut pas
    être vérifiée (cosign absent, bundle manquant, signature invalide).
- **Risque résiduel** : confiance dans le `.sha256` publié par la release
  GitHub (même origine que l'asset). En mode best-effort, l'absence de `cosign`
  réduit la mise à niveau au seul contrôle SHA-256 (pas de vérification
  cryptographique d'origine) ; le mode strict élimine ce risque au prix d'une
  dépendance à `cosign`. La **provenance SLSA** n'est pas vérifiée
  automatiquement côté client (vérification manuelle documentée). Un compromis de
  l'organisation GitHub resterait hors de portée de ces mitigations.

### M5 : SHA invalide / format `.sha256` inattendu
- **Menace** : fichier `.sha256` mal formé ou condensat erroné.
- **Mitigations** : parsing strict (`parse_sha256_file`) ; toute non-concordance
  **refuse** l'installation. (`upgrade_sha_invalide_refuse_installation`)
- **Risque résiduel** : aucun connu.

### M6 : Suppression non confirmée
- **Menace** : suppression silencieuse depuis un script / pipe.
- **Mitigations** : en mode non interactif (stdin non TTY), toute opération
  destructive sans `--yes` est **refusée** avec un message clair.
- **Risque résiduel** : usage explicite de `--yes` en CI (intentionnel).

### M7 : Injection dans `.bashrc`
- **Menace** : corruption du `.bashrc` (exécuté à chaque shell) lors de
  l'installation / désinstallation du bloc d'intégration.
- **Mitigations** : bloc délimité par des marqueurs uniques ; **sauvegarde**
  du `.bashrc` avant modification ; retrait idempotent et borné aux marqueurs
  (`remove_bashrc_block`, testé idempotent).
- **Risque résiduel** : négligeable.

### M8 : Fuite de commandes sensibles
- **Menace** : mots de passe / tokens capturés dans l'historique.
- **Mitigations** : filtrage à l'enregistrement et à l'import via mots-clés
  sensibles configurables ; nettoyage a posteriori de l'historique déjà stocké
  via `mnemo secrets scan` / `mnemo secrets redact` (dry-run par défaut,
  sauvegarde obligatoire avant écriture, seule la colonne `command` réécrite, et
  aucune valeur sensible jamais affichée en clair) ; la base reste locale ;
  aucune commande n'est transmise sur le réseau.
- **Risque résiduel** : la détection de `mnemo secrets` est heuristique et peut
  manquer un secret à la structure inhabituelle (jamais exfiltré). L'utilisateur
  peut le supprimer (`delete`), relancer une redaction ciblée ou enrichir
  `sensitive_keywords`. La redaction réécrit l'historique : une sauvegarde est
  conservée pour revenir en arrière.

### M9 : Erreur réseau GitHub
- **Menace** : indisponibilité, timeout, réponse inattendue lors de
  `update` / `upgrade`.
- **Mitigations** : erreurs contextualisées (statut HTTP vs transport), code de
  sortie non nul, aucun effet de bord destructif. `doctor` reste hors-ligne.
- **Risque résiduel** : aucun (mode dégradé propre).

### M10 : Exposition locale de l'historique via permissions trop ouvertes
- **Menace** : sur une machine multi-utilisateurs, un autre compte local lit la
  config, la base d'historique shell ou les archives de sauvegarde parce que ces
  fichiers ont été créés avec des permissions de groupe/autres (`644`, `664`…).
  Cas particulier : des **sauvegardes historiques** générées avant le
  durcissement (≤ v0.9.0) restent en `644` et exposent une copie complète de
  l'historique et de la configuration.
- **Mitigations** : sous Unix, config, `history.db` et archives `*.tar.gz` sont
  créées en `600` et les dossiers gérés en `700` (durcissement centralisé
  appliqué à la création et à `init`). `mnemo doctor` signale toute permission
  trop ouverte (y compris un résumé agrégé des archives de sauvegarde) et
  `mnemo doctor --fix` resserre à `600` la config, la base **et toutes les
  archives de sauvegarde existantes** (durcissement rétroactif) sans jamais lire,
  modifier ou supprimer le contenu des archives.
- **Risque résiduel** : un fichier déplacé/copié manuellement hors des chemins
  gérés, ou un `umask` exotique appliqué après coup, échappe au durcissement
  tant que `doctor --fix` n'est pas relancé ; les plateformes non-Unix reposent
  sur les ACL du système de fichiers.

## 4. Surface réseau

- **Sortant uniquement**, HTTPS, vers `api.github.com` et `github.com`
  (surchargeables via variables d'environnement pour les tests).
- Aucune écoute réseau. Aucune télémétrie.

## 5. Décisions de conception (local-first)

- **Pas de cloud, pas de compte, pas de télémétrie** : toutes les données
  restent sur la machine.
- **Sauvegarde avant destruction** : choix systématique privilégiant la
  récupérabilité sur la concision.
- **Signature des binaires : Sigstore (keyless), pas de GPG** : la chaîne de
  confiance repose sur HTTPS + SHA-256 (toujours obligatoire) publiés par la
  release GitHub, **complétée depuis la v0.8 par la vérification optionnelle de
  la signature Sigstore** de l'archive (`cosign verify-blob`, OIDC keyless, sans
  clé privée longue durée). La vérification est best-effort par défaut (pour ne
  pas casser les environnements minimaux sans `cosign`) et peut être rendue
  stricte (`--require-signature` / `MNEMO_REQUIRE_SIGNATURE=1`). GPG reste écarté
  (gestion de clés longue durée jugée plus lourde et moins auditable que le
  modèle keyless).
- **Linux uniquement** : réduit la surface et simplifie les hypothèses
  (permissions POSIX, `~/.bashrc`).

## 6. Vérification continue

- Tests unitaires et d'intégration couvrant chaque menace (cf. `INVARIANTS.md`).
- Chaîne DevSecOps : `cargo audit`, `cargo deny`, `cargo machete`, `gitleaks`
  (cf. `Makefile` cible `audit` et `.github/workflows/audit.yml`).
- Clippy en mode `-D warnings`, pas d'`unsafe` dans le code applicatif.

### Exceptions de supply chain documentées

- `RUSTSEC-2024-0436` (`paste`) est accepté temporairement : il s'agit d'une
  dépendance transitive via `ratatui`, signalée `unmaintained` (pas une
  vulnérabilité active). Suivi pour suppression lors d'une mise à jour future
  de Ratatui. Voir `deny.toml`, section `[advisories].ignore`.

## 7. Durcissement CI/CD et chaîne de release

Le pipeline GitHub Actions est conçu pour qu'**aucune release ne soit publiée
si un seul contrôle critique échoue** :

- **Gating strict.** Le workflow `release.yml` est séquencé en trois jobs :
  `quality` (fmt, clippy, tests, build glibc + musl) et `audit`
  (`cargo deny`, `cargo audit`, `gitleaks`) s'exécutent en amont ; le job
  `publish` déclare `needs: [quality, audit]` et `if: success()`. Si la qualité
  ou l'audit échoue, la publication n'est jamais atteinte. Aucun job critique
  n'utilise `continue-on-error`.
- **Permissions minimales.** Le workflow tourne en `contents: read` par défaut.
  Seul le job `publish` élève ses droits à `contents: write` et reçoit le token
  de publication ; les jobs de qualité et d'audit n'y ont jamais accès.
- **Checksums SHA-256 des assets.** `scripts/package-release.sh` génère pour
  chaque archive (`glibc` et `musl`) un fichier `.tar.gz.sha256` et le vérifie
  immédiatement (`sha256sum -c`). Un checksum manquant ou invalide interrompt
  le packaging et donc la publication. Les `.sha256` sont attachés à la Release
  aux côtés des archives (cf. `release-it.json`, liste d'assets).
- **Actions épinglées par SHA.** Toutes les actions tierces (`actions/checkout`,
  `actions/setup-node`, `actions/create-github-app-token`, `Swatinem/rust-cache`,
  `github/codeql-action`) sont référencées par SHA de commit complet, avec le tag
  lisible en commentaire et la procédure de mise à jour (`git ls-remote`). Cela
  empêche le déplacement silencieux d'un tag flottant. Ces actions sont maintenues
  sur des versions ciblant Node 24, ce qui supprime l'avertissement de dépréciation
  de Node 20 tout en conservant l'épinglage par SHA.
- **Versions de l'outillage figées (pas de canal flottant).**
  - **Rust** : `rust-toolchain.toml` épingle la version exacte du compilateur
    (`channel = "1.96.0"`), ses composants (`rustfmt`, `clippy`) et la cible
    `x86_64-unknown-linux-musl`. Le `rustup` pré-installé sur le runner lit ce
    fichier ; aucune action tierce n'installe la toolchain. Chaque job affiche
    `rustc --version` et `rustup show active-toolchain` comme preuve.
  - **Node.js** : `.node-version` épingle la version (`24.15.0`), consommée par
    `setup-node` via `node-version-file` (pas de `node-version: 20` ni `latest`).
  - **Outils d'audit Cargo** : `cargo-audit`, `cargo-deny`, `cargo-machete` sont
    installés en version exacte (`--version X.Y.Z --locked`) et leurs versions
    sont affichées en CI.
  - **Outils d'intégrité d'artefacts** : `cargo-cyclonedx` (SBOM) est installé
    en version exacte (`--version 0.5.9 --locked`) ; `cosign` (signature +
    provenance) est installé depuis un binaire dont l'empreinte SHA-256 est
    vérifiée (version `v3.1.1` épinglée). Leurs versions sont affichées en CI.
- **SBOM, signatures et provenance des artefacts.** Le job `publish` produit,
  via les hooks `after:bump` de release-it (donc **avant** la création de la
  release) :
  - un **SBOM CycloneDX JSON** (`scripts/generate-sbom.sh`, `cargo-cyclonedx`),
    validé (champs `bomFormat`/`specVersion`/`components`) et accompagné de son
    empreinte SHA-256 ;
  - un fichier de **checksums agrégés** couvrant les deux archives et le SBOM
    (`scripts/checksums-release.sh`), vérifié avant signature ;
  - pour chaque artefact, une **signature `cosign`** (keyless, OIDC ambiant
    GitHub Actions, aucun secret long terme) et une **attestation de
    provenance SLSA v1** (`cosign attest-blob`), toutes deux **re-vérifiées**
    par `cosign verify-blob` / `verify-blob-attestation`
    (`scripts/sign-release.sh`).

  Ces étapes sont **fail-close** (`set -euo pipefail`) : tout échec de
  génération, de signature, d'attestation ou de vérification interrompt
  release-it, donc **aucune release n'est créée**. Le keyless requiert
  `id-token: write`, accordé uniquement au job `publish`. Les bundles Sigstore
  (`*.sigstore.json`, `*.provenance.sigstore.json`) sont attachés à la Release.
- **Runners épinglés.** Les workflows utilisent `ubuntu-24.04` (et
  `ubuntu-22.04` là où le lien glibc 2.35 de l'asset GNU l'impose) plutôt que
  `ubuntu-latest`. Ces images GitHub restent maintenues et moins flottantes que
  `latest`, **mais ne sont pas immuables au digest** : c'est une limite
  résiduelle assumée (cf. § risques résiduels).
- **Téléchargements vérifiés.** Le binaire `gitleaks` est épinglé en version et
  son empreinte SHA-256 est contrôlée avant exécution (pas de `curl | bash`, pas
  d'installation sans checksum). Le binaire `cosign` (v3.1.1) est installé de
  la même façon : empreinte SHA-256 vérifiée avant installation.
- **Lockfiles obligatoires.** `Cargo.lock` et `package-lock.json` sont
  versionnés. La CI utilise `cargo fetch --locked` puis `cargo build/test/clippy
  --locked` (aucune mise à jour implicite de dépendances) et `npm ci`
  (jamais `npm install`), complété par `npm audit --omit=dev` avant publication.
- **Publication contrôlée.** La release réelle n'est déclenchée que par un push
  sur `main` (typiquement un merge de PR), après CI verte. En local,
  `make release-check` n'exécute que `release-it --dry-run` : aucune publication
  réelle ne peut partir d'un poste de développement.

### Limites résiduelles de la supply chain CI

- Les runners GitHub hébergés (`ubuntu-24.04`, `ubuntu-22.04`) sont des images
  **mises à jour en place** : leur contenu évolue sans changement de label et
  n'est pas épinglé à un digest. On accepte cette confiance dans l'infrastructure
  GitHub ; un durcissement supplémentaire (runners conteneurisés épinglés par
  digest) reste possible mais hors périmètre pour un projet mono-utilisateur.
- Le toolchain Rust est téléchargé par `rustup` via la version épinglée ; on fait
  confiance à l'infrastructure de distribution officielle de Rust (signée).
- La **provenance** atteste l'origine CI (dépôt, workflow, commit, run) mais
  n'est pas un niveau SLSA Build L3 formellement certifié : le build n'est pas
  isolé sur un constructeur dédié inviolable. Le prédicat reflète le contexte
  GitHub Actions ; il faut faire confiance à ce contexte.
- Depuis la **v0.8**, `install.sh` et `mnemo upgrade` vérifient
  **automatiquement la signature Sigstore** de l'archive (`cosign verify-blob`)
  lorsque `cosign` est présent, en plus du contrôle SHA-256 toujours obligatoire.
  La vérification est best-effort par défaut (avertissement si `cosign` est
  absent, pour préserver les environnements minimaux) et peut être rendue
  bloquante via `--require-signature` / `MNEMO_REQUIRE_SIGNATURE=1`. `cosign`
  n'est jamais téléchargé automatiquement par l'installateur. La vérification de
  la **provenance** (`*.provenance.sigstore.json`) reste, elle, une **étape
  manuelle** documentée côté utilisateur ; seule la signature de l'archive est
  contrôlée automatiquement.