diff --git a/documentation/docs/architecture.md b/documentation/docs/architecture.md index fa2e767..475e0bc 100644 --- a/documentation/docs/architecture.md +++ b/documentation/docs/architecture.md @@ -15,7 +15,7 @@ Deux composants distincts dans un même dépôt, industrialisés par une **uniqu │ └── vercel.json ├── .github/ │ ├── workflows/ci-cd.yml # pipeline principal durci (staging + main) -│ ├── actions/trivy-scan/ # composite action — scan SBOM CycloneDX +│ ├── actions/trivy-scan/ # composite action : scan SBOM CycloneDX │ ├── branch-protection/main.yml # politique de protection documentée │ └── secrets-prod.yaml # secrets chiffrés SOPS (valeurs ENC[...]) ├── scripts/ diff --git a/documentation/docs/assets/Gitleaks.png b/documentation/docs/assets/Gitleaks.png new file mode 100644 index 0000000..dcce9fa Binary files /dev/null and b/documentation/docs/assets/Gitleaks.png differ diff --git a/documentation/docs/assets/cd.png b/documentation/docs/assets/cd.png new file mode 100644 index 0000000..9ddfcff Binary files /dev/null and b/documentation/docs/assets/cd.png differ diff --git a/documentation/docs/assets/ci-cd.webp b/documentation/docs/assets/ci-cd.webp new file mode 100644 index 0000000..8aa2302 Binary files /dev/null and b/documentation/docs/assets/ci-cd.webp differ diff --git a/documentation/docs/assets/ci.png b/documentation/docs/assets/ci.png new file mode 100644 index 0000000..4b293c3 Binary files /dev/null and b/documentation/docs/assets/ci.png differ diff --git a/documentation/docs/assets/codeql.png b/documentation/docs/assets/codeql.png new file mode 100644 index 0000000..a31c16a Binary files /dev/null and b/documentation/docs/assets/codeql.png differ diff --git a/documentation/docs/assets/docker.png b/documentation/docs/assets/docker.png new file mode 100644 index 0000000..950bf7c Binary files /dev/null and b/documentation/docs/assets/docker.png differ diff --git a/documentation/docs/assets/github.png b/documentation/docs/assets/github.png new file mode 100644 index 0000000..35f2ff0 Binary files /dev/null and b/documentation/docs/assets/github.png differ diff --git a/documentation/docs/assets/github_actions.png b/documentation/docs/assets/github_actions.png new file mode 100644 index 0000000..b9084da Binary files /dev/null and b/documentation/docs/assets/github_actions.png differ diff --git a/documentation/docs/assets/github_pages.png b/documentation/docs/assets/github_pages.png new file mode 100644 index 0000000..3657178 Binary files /dev/null and b/documentation/docs/assets/github_pages.png differ diff --git a/documentation/docs/assets/logo_ynov_campus_sophia.png b/documentation/docs/assets/logo_ynov_campus_sophia.png new file mode 100644 index 0000000..4ccc83a Binary files /dev/null and b/documentation/docs/assets/logo_ynov_campus_sophia.png differ diff --git a/documentation/docs/assets/nodejs.png b/documentation/docs/assets/nodejs.png new file mode 100644 index 0000000..09b73b9 Binary files /dev/null and b/documentation/docs/assets/nodejs.png differ diff --git a/documentation/docs/assets/ram.png b/documentation/docs/assets/ram.png new file mode 100644 index 0000000..5e1bc1b Binary files /dev/null and b/documentation/docs/assets/ram.png differ diff --git a/documentation/docs/assets/skeleton-projet-final.zip b/documentation/docs/assets/skeleton-projet-final.zip new file mode 100644 index 0000000..517e087 Binary files /dev/null and b/documentation/docs/assets/skeleton-projet-final.zip differ diff --git a/documentation/docs/assets/sops.png b/documentation/docs/assets/sops.png new file mode 100644 index 0000000..df0f3eb Binary files /dev/null and b/documentation/docs/assets/sops.png differ diff --git a/documentation/docs/assets/sujet-projet-final.pdf b/documentation/docs/assets/sujet-projet-final.pdf new file mode 100644 index 0000000..2bfd1f3 Binary files /dev/null and b/documentation/docs/assets/sujet-projet-final.pdf differ diff --git a/documentation/docs/assets/trivy.png b/documentation/docs/assets/trivy.png new file mode 100644 index 0000000..f7d5bdc Binary files /dev/null and b/documentation/docs/assets/trivy.png differ diff --git a/documentation/docs/assets/vercel.png b/documentation/docs/assets/vercel.png new file mode 100644 index 0000000..e6177c5 Binary files /dev/null and b/documentation/docs/assets/vercel.png differ diff --git a/documentation/docs/ci.md b/documentation/docs/ci.md index db2486d..1e0c397 100644 --- a/documentation/docs/ci.md +++ b/documentation/docs/ci.md @@ -1,5 +1,7 @@ # Pipeline CI durci +![Intégration continue](assets/ci.png){ .logo } + Le workflow [`ci-cd.yml`](https://github.com/Sorway/DevSecOps/blob/main/.github/workflows/ci-cd.yml) agit comme une **barrière** : rien ne se déploie sans le succès complet des contrôles. diff --git a/documentation/docs/composite-action.md b/documentation/docs/composite-action.md index e6a388b..2c5228f 100644 --- a/documentation/docs/composite-action.md +++ b/documentation/docs/composite-action.md @@ -1,4 +1,4 @@ -# Composite Action — Trivy SBOM +# Composite Action : Trivy SBOM Pour centraliser la gouvernance de sécurité et éviter la redondance, le scan de dépendances au format SBOM est isolé dans une **action composite** locale, réutilisable en boîte noire. @@ -11,6 +11,8 @@ et expose des entrées / sorties strictes. ## L'action `trivy-scan` +![Trivy](assets/trivy.png){ .logo } + Fichier [`.github/actions/trivy-scan/action.yml`](https://github.com/Sorway/DevSecOps/blob/main/.github/actions/trivy-scan/action.yml). ```yaml diff --git a/documentation/docs/conformite.md b/documentation/docs/conformite.md index 3bb2d92..29e9ed1 100644 --- a/documentation/docs/conformite.md +++ b/documentation/docs/conformite.md @@ -1,63 +1,228 @@ # Conformité aux consignes -Récapitulatif exigence par exigence, avec le lien vers l'implémentation. :material-check-decagram: +Chaque exigence de l'énoncé, avec **sa preuve** : extrait de code réel, configuration, ou +déploiement en ligne. Les blocs « Preuve » sont repliés, cliquez pour les ouvrir. + +[![CI CD durcie](https://github.com/Sorway/DevSecOps/actions/workflows/ci-cd.yml/badge.svg?branch=main)](https://github.com/Sorway/DevSecOps/actions/workflows/ci-cd.yml) + +**Preuves en ligne :** [Frontend](https://sorway.github.io/DevSecOps/) · +[API `/api/health`](https://projet-final-inky-iota.vercel.app/api/health) · +[Dépôt](https://github.com/Sorway/DevSecOps) · +[Image GHCR](https://github.com/Sorway/DevSecOps/pkgs/container/devsecops%2Fbackend) ## Gouvernance Git -- [x] `staging` pivot d'intégration, CI complète à chaque événement — [détails](gouvernance.md) +- [x] `staging` pivot d'intégration, CI complète à chaque événement - [x] `main` production, push directs interdits (ruleset `REVIEW_REQUIRED`) -- [x] `on: push/pull_request: [staging, main]` -- [x] Blocs `if: github.ref == 'refs/heads/main'` + matrice `needs` stricte -- [x] Job de production lié à un `environment` nommé (`production` / `github-pages`) +- [x] Déclenché sur `staging` **et** `main`, `if` sur `main`, `needs` strict, `environment` nommé + +??? example "Preuve : déclenchement, gating conditionnel et environnement (`ci-cd.yml`)" + + ```yaml + on: + push: + branches: [staging, main] # déclenché sur les deux branches + pull_request: + branches: [staging, main] + + jobs: + deploy-backend: + if: github.ref == 'refs/heads/main' # bloc conditionnel de job + needs: [test, gitleaks, codeql, sbom-scan, docker-image] # dépendance stricte + environment: production # environnement nommé + ``` + + La protection de `main` est **effective** : toute PR y est en état `REVIEW_REQUIRED` et ne peut + être fusionnée sans revue (ruleset GitHub). + +## Durcissement local (Shift-Left) + +- [x] Hook `pre-commit` : `actionlint` + `gitleaks --staged` (bloquants) +- [x] Refus des fichiers `.env` / `.pem` / `.key` avec message rouge, commit avorté +- [x] Règle Gitleaks sur-mesure `SECWALLET_` + 24 caractères + entropie + +??? example "Preuve : contrôle de structure du hook (`scripts/pre-commit.sh`)" + + ```bash + if [[ "$file" == *.env || "$file" == *.pem || "$file" == *.key ]]; then + echo -e "${RED}Sécurité : Tentative de commit d'un fichier de configuration ou d'une clé en clair. Opération annulée.${RESET}" + exit 1 + fi + ``` -## Durcissement local +??? example "Preuve : règle Gitleaks sur-mesure (`gitleaks.toml`)" -- [x] Hook `pre-commit` : `actionlint` + `gitleaks --staged` (bloquants) — [détails](shift-left.md) -- [x] Contrôle de structure `.env` / `.pem` / `.key` + message rouge, commit avorté -- [x] Règle Gitleaks sur-mesure `SECWALLET_[A-Z0-9]{24}` + entropie + ```toml + [[rules]] + id = "secwallet-internal-token" + regex = '''SECWALLET_[A-Z0-9]{24}''' + entropy = 3.5 + keywords = ["SECWALLET_"] + ``` -## Secrets par enveloppe +## Secrets par enveloppe (SOPS / age) -- [x] Paire de clés **age** (privée `ops.txt`, ignorée par Git) — [détails](secrets.md) -- [x] `.github/secrets-prod.yaml` chiffré SOPS : valeurs `ENC[...]`, clés lisibles (`encrypted_regex`) -- [x] Déchiffrement runtime **en RAM**, aucun secret sur le disque du runner +- [x] Paire de clés **age**, privée `ops.txt` (ignorée par Git) +- [x] Chiffrement SOPS **sélectif** : valeurs `ENC[...]`, clés lisibles +- [x] Déchiffrement runtime **en RAM**, aucun secret sur le disque + +??? example "Preuve : chiffrement sélectif (`.github/secrets-prod.yaml`)" + + ```yaml + production: + DATABASE_URL: ENC[AES256_GCM,data:TCwiv...,type:str] # valeur chiffrée + JWT_SECRET: ENC[AES256_GCM,data:5bSz4...,type:str] + API_KEY: ENC[AES256_GCM,data:oV5AU...,type:str] + sops: + encrypted_regex: ^(DATABASE_URL|JWT_SECRET|API_KEY)$ # seules ces valeurs + ``` + +??? example "Preuve : déchiffrement en RAM, sans fichier (`ci-cd.yml`, job `deploy-backend`)" + + ```bash + export SOPS_AGE_KEY # clé en mémoire seulement + secrets_json="$(sops -d --output-type json .github/secrets-prod.yaml)" # déchiffré en RAM + # aucun mktemp, aucun SOPS_AGE_KEY_FILE : rien n'est écrit sur le disque du runner + ``` ## Conteneurisation & GHCR -- [x] `Dockerfile` multi-stage, non-root (`node:24-alpine`) — [détails](conteneurisation.md) -- [x] Build conditionné au filtrage de chemins (`dorny/paths-filter`) -- [x] Scan de l'image (Trivy `HIGH,CRITICAL`) avant publication -- [x] Publication GHCR conditionnelle (scan OK + `main`), tag = SHA du commit +- [x] `Dockerfile` multi-stage, non-root (`node:24-alpine`) +- [x] Build conditionné au filtrage de chemins +- [x] Scan Trivy de l'image avant publication +- [x] Publication GHCR conditionnelle (scan OK + `main`), tag = SHA + +??? example "Preuve : Dockerfile multi-stage non-root (`backend/Dockerfile`)" + + ```dockerfile + FROM node:24-alpine AS deps + WORKDIR /app + COPY package*.json ./ + RUN npm ci --omit=dev + + FROM node:24-alpine AS runtime + RUN apk upgrade --no-cache \ + && addgroup -S nodeapp && adduser -S nodeapp -G nodeapp + COPY --from=deps /app/node_modules ./node_modules + USER nodeapp + ``` + +??? example "Preuve : filtrage, scan et push conditionnel (`ci-cd.yml`, job `docker-image`)" + + ```yaml + - uses: dorny/paths-filter@v4 + id: filter + with: + filters: | + backend: + - 'backend/**' + - '.github/workflows/ci-cd.yml' + - name: Scan backend image + if: steps.filter.outputs.backend == 'true' + run: trivy image --exit-code 1 --severity HIGH,CRITICAL "${{ steps.meta.outputs.image }}" + - name: Push backend image to GHCR + if: github.ref == 'refs/heads/main' && steps.filter.outputs.backend == 'true' + run: docker push "ghcr.io/${GITHUB_REPOSITORY,,}/backend:${GITHUB_SHA}" + ``` ## Pipeline CI durci -- [x] `permissions: contents: read` global, écritures isolées au job — [détails](ci.md) +- [x] `permissions: contents: read` global, écritures isolées au job - [x] Cache des dépendances Node -- [x] CodeQL + upload SARIF, échec sur `High`/`Error` — [détails](sast.md) -- [x] Tests bloquants ; Gitleaks exit ≠ 0 ; **aucun `continue-on-error: true`** +- [x] CodeQL + upload SARIF, échec sur `High`/`Error` +- [x] Tests bloquants, Gitleaks exit ≠ 0, **aucun `continue-on-error`** + +??? example "Preuve : moindre privilège (`ci-cd.yml`)" + + ```yaml + permissions: + contents: read # global : lecture seule + + jobs: + docker-image: + permissions: + packages: write # écriture isolée à ce job + codeql: + permissions: + security-events: write + ``` + +??? example "Preuve : CodeQL fait échouer sur une faille majeure (`ci-cd.yml`)" + + ```yaml + - name: Fail on CodeQL high severity + run: | + jq -e '[ .runs[].results[] + | select((.level=="error") or (.properties["security-severity"] // "0" | tonumber >= 7.0)) + ] | length == 0' codeql-results/javascript.sarif + ``` ## Composite Action -- [x] `.github/actions/trivy-scan/action.yml` de type composite — [détails](composite-action.md) -- [x] Entrée obligatoire = chemin du SBOM CycloneDX JSON +- [x] `.github/actions/trivy-scan/action.yml` de type composite +- [x] Entrée obligatoire = chemin du SBOM CycloneDX - [x] `trivy sbom` : échec sur `CRITICAL`, avertissement sur `HIGH`/`MEDIUM` -- [x] Installe Trivy elle-même (boîte noire réutilisable) +- [x] Installe Trivy elle-même (boîte noire) + +??? example "Preuve : action composite (`.github/actions/trivy-scan/action.yml`)" + + ```yaml + inputs: + sbom-path: + required: true # entrée obligatoire + runs: + using: composite + steps: + - name: Ensure Trivy is installed # boîte noire : elle s'installe seule + shell: bash + run: command -v trivy >/dev/null 2>&1 || { curl -sfL ... ; sudo install trivy /usr/local/bin/ ; } + - name: Scan CRITICAL vulnerabilities + shell: bash + run: trivy sbom --exit-code 1 --severity CRITICAL --format table "${{ inputs.sbom-path }}" + - name: Summarize HIGH and MEDIUM # avertissement seulement + shell: bash + run: trivy sbom --exit-code 0 --severity HIGH,MEDIUM ... >> "$GITHUB_STEP_SUMMARY" + ``` ## Déploiement continu -- [x] Frontend GitHub Pages, `main` uniquement, OIDC (`pages` + `id-token: write`) — [détails](deploiement.md) +- [x] Frontend GitHub Pages, `main` uniquement, OIDC (`pages` + `id-token: write`) - [x] `upload-pages-artifact` du `/frontend` + `deploy-pages` (hermétique) -- [x] Backend Vercel en CLI, `needs` sécurité + livraison -- [x] Déchiffrement SOPS en RAM, injection à la volée (`--env`) +- [x] Backend Vercel, `needs` sécurité + livraison, secrets en RAM + +??? example "Preuve : frontend en OIDC (`ci-cd.yml`, job `deploy-frontend`)" + + ```yaml + if: github.ref == 'refs/heads/main' + needs: [test, gitleaks, codeql, sbom-scan, docker-image] + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + permissions: + pages: write + id-token: write + ``` + +??? example "Preuve : backend Vercel, secrets injectés à la volée (`ci-cd.yml`)" + + ```bash + vercel pull --yes --environment=production --token "$VERCEL_TOKEN" + vercel deploy backend --prod --yes --token "$VERCEL_TOKEN" "${env_args[@]}" # --env=K=V en RAM + ``` ## Robustesse -- [x] Concurrence annulante (`cancel-in-progress: true`) — [détails](robustesse.md) +- [x] Concurrence annulante (`cancel-in-progress: true`) - [x] Healthcheck `curl --fail /api/health`, échec si ≠ 200 ---- +??? example "Preuve : concurrence et healthcheck (`ci-cd.yml`)" + + ```yaml + concurrency: + group: ci-cd-${{ github.ref }} + cancel-in-progress: true -!!! success "Livrables" - - **Frontend** : [sorway.github.io/DevSecOps](https://sorway.github.io/DevSecOps/) - - **API** : [projet-final-inky-iota.vercel.app](https://projet-final-inky-iota.vercel.app) (`/api/health`) - - **Dépôt** : [github.com/Sorway/DevSecOps](https://github.com/Sorway/DevSecOps) + # ... en fin de job deploy-backend : + - name: Healthcheck + run: curl --fail --silent --show-error "${{ steps.vercel.outputs.url }}/api/health" + ``` diff --git a/documentation/docs/conteneurisation.md b/documentation/docs/conteneurisation.md index 2a2f404..6b078c1 100644 --- a/documentation/docs/conteneurisation.md +++ b/documentation/docs/conteneurisation.md @@ -1,5 +1,7 @@ # Conteneurisation & GHCR +![Docker](assets/docker.png){ .logo } + L'artefact serveur du backend est construit et publié en image Docker **sécurisée**. ## Dockerfile multi-stage non-root diff --git a/documentation/docs/contexte.md b/documentation/docs/contexte.md index 618b51d..0bd7eb6 100644 --- a/documentation/docs/contexte.md +++ b/documentation/docs/contexte.md @@ -1,72 +1,349 @@ # Contexte & consignes -## Le sujet +Les documents originaux fournis pour ce projet : -**Industrialisation, durcissement et architecture CI/CD.** Une équipe de développement nous confie -le dépôt d'une application scindée en deux composants au sein d'un même dépôt : +[:material-file-pdf-box: Sujet original (PDF)](assets/sujet-projet-final.pdf){ .md-button .md-button--primary } +[:material-folder-zip: Base de code fournie (ZIP)](assets/skeleton-projet-final.zip){ .md-button } -- **`frontend/`** : une Single Page Application entièrement statique (HTML, CSS, JavaScript moderne) - qui consomme l'API. -- **`backend/`** : une API REST Node.js / Express manipulant des **données hautement sensibles**, - nécessitant des clés d'API et des accès à des infrastructures externes. +## Le point de départ -!!! quote "Objectif" - Structurer la gouvernance Git pour **interdire toute action non auditée** et bâtir un flux où le - code de production est **techniquement validé** avant son déploiement. +On nous confie un dépôt qui contient déjà l'application. Notre travail n'est donc pas d'écrire le +code métier, mais de **l'industrialiser** : bâtir autour de lui une chaîne d'intégration et de +déploiement qui rende chaque action tracée, vérifiée et reproductible. En clair, faire passer un +projet du stade « ça marche sur ma machine » à celui d'une usine logicielle auditable. -## Les exigences, par thème +L'application a deux visages : un **frontend** (une SPA statique qui consomme l'API) et un +**backend** (une API Node.js / Express qui manipule des données sensibles, avec des clés d'API et +des accès à des infrastructures externes). Ce sont deux mondes très différents à livrer, donc on les +traite séparément dans une même chaîne. -=== "Gouvernance" +Tout le projet tient dans une phrase, qu'on a gardée en tête à chaque décision : - - `staging` = branche pivot d'intégration (CI complète à chaque événement). - - `main` = production, **push directs interdits**. - - Workflow déclenché sur `staging` **et** `main`, blocs `if: github.ref == 'refs/heads/main'`, - matrice `needs` stricte, job de prod lié à un `environment` nommé. +> **Aucun code n'atteint la production sans avoir été techniquement validé.** -=== "Durcissement local" +```mermaid +flowchart LR + subgraph POSTE["Sur le poste"] + code[Écriture du code]:::actor --> hook[Hook pre-commit]:::warn + end + subgraph GH["Sur GitHub"] + hook --> stg[(staging)]:::info + stg --> ci[CI durcie]:::info + ci --> gate{Tout est vert ?}:::warn + gate -->|oui, via PR + revue| main[(main)]:::prod + gate -->|non| stop[Bloqué]:::danger + end + subgraph PROD["En production"] + main --> pages[GitHub Pages]:::ok + main --> vercel[Vercel]:::ok + end + classDef actor fill:#495057,stroke:#343a40,color:#fff; + classDef info fill:#1c7ed6,stroke:#1971c2,color:#fff; + classDef warn fill:#f59f00,stroke:#e8590c,color:#fff; + classDef ok fill:#12b886,stroke:#0ca678,color:#fff; + classDef prod fill:#0b7285,stroke:#095c6b,color:#fff; + classDef danger fill:#e03131,stroke:#c92a2a,color:#fff; +``` - - Hook `pre-commit` : `actionlint` sur les workflows + `gitleaks` sur les modifications indexées. - - Règle Gitleaks sur-mesure : jetons `SECWALLET_` + 24 caractères + entropie. - - Refus des fichiers `.env` / `.pem` / `.key` dans l'index (message rouge, commit avorté). +Chaque étage ci-dessus est une **garantie** ajoutée. On détaille maintenant chaque thème selon la +même démarche : le besoin auquel il répond, ce qu'on a vu en cours et les bonnes pratiques associées, +les contraintes précises de l'énoncé, ce qu'on a concrètement mis en place, et enfin les points à ne +pas oublier. -=== "Secrets" +## 1. La gouvernance des branches - - Paire de clés **age** (privée nommée `ops.txt`). - - `.github/secrets-prod.yaml` chiffré via **SOPS** : seules les **valeurs** deviennent `ENC[...]`, - les **clés** YAML restent lisibles (`git diff` propres). +![GitHub](assets/github.png){ .logo-md } -=== "Conteneurisation" +
- - `Dockerfile` multi-stage (bonnes pratiques). - - Build Docker conditionné au **filtrage de chemins**. - - Scan de l'image (Trivy) **avant** publication. - - Publication sur **GHCR** seulement si le scan réussit, image taggée au **SHA** du commit. +- 🎯 Prise en compte du besoin dans une équipe, plusieurs personnes touchent au même code en + même temps. Sans règles, n'importe qui pourrait envoyer directement en production une modification + non testée, ou écraser le travail d'un autre. On voulait une organisation où la production reste + intouchable et où toute l'équipe se synchronise sur une base commune, sans conflit. +- 🎓 Vu en cours et bonnes pratiques un dépôt sérieux repose sur un flux de branches clair, + souvent une variante allégée du Git Flow : une branche d'**intégration** où tout converge, et une + branche de **production verrouillée**. La bonne pratique clé, c'est que rien n'atteint la production + sans une **Pull Request relue par un pair**, garde-fou humain en plus des contrôles automatiques. +- 📋 Contraintes de l'énoncé le workflow doit se déclencher sur `staging` **et** `main`, les + jobs de déploiement portent un `if` sur `main`, une matrice `needs` stricte les enchaîne, et le job + de production est rattaché à un `environment` nommé. +- 🛠️ Ce qu'on met en place `staging` en branche par défaut, `main` protégée par un ruleset + GitHub qui exige une revue avant fusion, et la politique rendue lisible directement dans le YAML. +- ⚠️ À ne pas oublier le push direct sur `main` est **interdit** (pas seulement déconseillé), la + politique doit être **visible dans le YAML**, et le job de prod porte un `environment` nommé. -=== "CI durci" +
- - `permissions: contents: read` global ; privilèges d'écriture isolés au job. - - Cache des dépendances Node. - - **CodeQL** (SARIF) ; échec si vulnérabilité `High`/`Error`. - - Tests bloquants ; Gitleaks exit ≠ 0 ; **`continue-on-error: true` interdit**. +```mermaid +flowchart LR + feat[branche de travail]:::info -->|Pull Request| stg[(staging)]:::info + stg -->|CI complète à chaque push| check[Validation]:::warn + check -->|PR + revue obligatoire| main[(main)]:::prod + main -.->|push direct| blocked[Interdit par le ruleset]:::danger + classDef info fill:#1c7ed6,stroke:#1971c2,color:#fff; + classDef warn fill:#f59f00,stroke:#e8590c,color:#fff; + classDef prod fill:#0b7285,stroke:#095c6b,color:#fff; + classDef danger fill:#e03131,stroke:#c92a2a,color:#fff; +``` -=== "Composite Action" +## 2. Le durcissement local (Shift-Left) - - `.github/actions/trivy-scan/action.yml` de type **composite**. - - Entrée obligatoire : chemin du SBOM **CycloneDX JSON**. - - Échec **uniquement** sur `CRITICAL` ; `HIGH`/`MEDIUM` → avertissement dans le résumé. +![GitHub Actions](assets/github_actions.png){ .logo-md } -=== "Déploiement & robustesse" +
- - Frontend → **GitHub Pages** (OIDC, artefacts éphémères). - - Backend → **Vercel** (déchiffrement SOPS en RAM, aucun secret sur disque). - - Concurrence annulante ; **healthcheck** `/api/health` post-déploiement. +- 🎯 Prise en compte du besoin plus une erreur est détectée tôt, moins elle coûte cher. Un + secret oublié ou un workflow invalide qui atteint GitHub, c'est déjà une trace publique et du temps + perdu. On voulait un premier filet **sur le poste du développeur**, avant même le premier `push`. +- 🎓 Vu en cours et bonnes pratiques c'est le principe du **Shift-Left**, « décaler vers la + gauche » les contrôles de sécurité, c'est-à-dire les rapprocher du moment où le code est écrit. + L'outil standard est le **hook Git `pre-commit`**, un script qui s'exécute avant chaque commit et + peut le refuser. +- 📋 Contraintes de l'énoncé valider séquentiellement `actionlint` puis `gitleaks` **sur les + fichiers indexés uniquement**, refuser tout `.env` / `.pem` / `.key` avec un message rouge, et + s'appuyer sur une règle Gitleaks sur-mesure pour les jetons `SECWALLET_`. +- 🛠️ Ce qu'on met en place un hook **versionné** (auditable et réinstallable), trois barrières + bloquantes successives, et une règle `SECWALLET_[A-Z0-9]{24}` couplée à une vérification d'entropie. +- ⚠️ À ne pas oublier le message rouge est imposé **au mot près**, gitleaks ne scanne que le + **staged**, la règle veut **exactement 24 caractères**, et le hook doit **réellement bloquer**. -## Livrables +
-Un fichier texte avec les **URLs Vercel et GitHub Pages** et l'URL du **dépôt public** contenant -l'arborescence complète (frontend, backend, hooks locaux, structure `.github/`) montrant la -protection des branches, le filtrage Docker et le chaînage des dépendances. +```mermaid +flowchart TB + start([git commit]):::info --> struct{Fichier .env / .pem / .key indexé ?}:::warn + struct -->|oui| abort[Commit avorté, message rouge]:::danger + struct -->|non| lint{actionlint OK ?}:::warn + lint -->|non| abort + lint -->|oui| leaks{gitleaks : aucun secret ?}:::warn + leaks -->|non| abort + leaks -->|oui| ok([Commit autorisé]):::ok + classDef info fill:#1c7ed6,stroke:#1971c2,color:#fff; + classDef warn fill:#f59f00,stroke:#e8590c,color:#fff; + classDef danger fill:#e03131,stroke:#c92a2a,color:#fff; + classDef ok fill:#12b886,stroke:#0ca678,color:#fff; +``` -!!! success "Vérification" - La page [Conformité aux consignes](conformite.md) reprend chaque exigence et pointe vers son - implémentation dans le dépôt. +## 3. Les secrets par enveloppe + +![SOPS](assets/sops.png){ .logo-md } + +
+ +- 🎯 Prise en compte du besoin une API qui manipule des données sensibles a besoin de secrets + (URL de base de données, clé JWT, clés d'API). Le réflexe naïf serait un fichier `.env`, mais il + fuirait dans l'historique Git. On voulait **versionner** ces secrets sans jamais les exposer. +- 🎓 Vu en cours et bonnes pratiques le **chiffrement par enveloppe** dans une logique + **GitOps** : stocker les secrets **chiffrés** dans le dépôt et ne les déchiffrer qu'au dernier + moment, au runtime. On utilise **age** pour la paire de clés et **SOPS** pour chiffrer le fichier. +- 📋 Contraintes de l'énoncé clé privée nommée `ops.txt`, chiffrement **des seules valeurs** + (clés YAML lisibles pour des `git diff` propres), et déchiffrement en CD **en RAM**, sans jamais + écrire de secret sur le disque du runner. +- 🛠️ Ce qu'on met en place une paire `age`, un `encrypted_regex` qui ne cible que les valeurs, + et la clé `SOPS_AGE_KEY` lue directement depuis une variable d'environnement, donc en mémoire. +- ⚠️ À ne pas oublier `ops.txt` reste **hors du dépôt**, **seules les valeurs** sont chiffrées, + et **aucun fichier de secret en clair** ne doit toucher le disque (donc pas de `mktemp`). + +
+ +```mermaid +flowchart LR + clair[secrets-prod.yaml en clair]:::warn -->|sops chiffre les valeurs| enc[Valeurs en ENC..., clés lisibles]:::info + enc -->|commité sans risque| repo[(Dépôt)]:::info + repo -->|au déploiement| ram[Déchiffrement en RAM]:::ok + ram -->|injecté à la volée| vercel[Vercel]:::prod + ram -.->|jamais| disk[Fichier sur disque]:::danger + classDef warn fill:#f59f00,stroke:#e8590c,color:#fff; + classDef info fill:#1c7ed6,stroke:#1971c2,color:#fff; + classDef ok fill:#12b886,stroke:#0ca678,color:#fff; + classDef prod fill:#0b7285,stroke:#095c6b,color:#fff; + classDef danger fill:#e03131,stroke:#c92a2a,color:#fff; +``` + +## 4. La conteneurisation et GHCR + +![Docker](assets/docker.png){ .logo-md } + +
+ +- 🎯 Prise en compte du besoin pour livrer le backend de façon fiable, il faut qu'il tourne + partout pareil. On voulait un artefact reproductible (une image Docker), sans le reconstruire à + chaque fois qu'on modifie une virgule du frontend. +- 🎓 Vu en cours et bonnes pratiques le **multi-stage build** (on installe dans un premier + étage, on ne garde que le nécessaire dans l'image finale, plus légère et avec moins de surface + d'attaque), l'exécution **non-root**, le **scan de vulnérabilités avant publication**, et un tag + **immuable** (le SHA) plutôt que `latest`. +- 📋 Contraintes de l'énoncé image multi-stage, build **conditionné au filtrage de chemins**, + scan Trivy **avant** publication, et push GHCR seulement si le scan est clean, taggé au SHA. +- 🛠️ Ce qu'on met en place un `Dockerfile` à deux étages (`deps` puis `runtime`), un + utilisateur dédié `nodeapp`, un filtre `dorny/paths-filter`, un `trivy image` bloquant, et un push + conditionnel. +- ⚠️ À ne pas oublier multi-stage **et** non-root, rebuild **seulement si** le backend change, + scan **avant** le push, et tag **= SHA** (pas `latest`). + +
+ +```mermaid +flowchart LR + change{backend modifié ?}:::warn -->|non| skip[On ne rebuild pas]:::info + change -->|oui| build[docker build multi-stage]:::info + build --> scan{Trivy : HIGH / CRITICAL ?}:::warn + scan -->|vulnérable| fail[Échec, aucun push]:::danger + scan -->|clean ET branche main| push[Push GHCR taggé au SHA]:::ok + classDef warn fill:#f59f00,stroke:#e8590c,color:#fff; + classDef info fill:#1c7ed6,stroke:#1971c2,color:#fff; + classDef danger fill:#e03131,stroke:#c92a2a,color:#fff; + classDef ok fill:#12b886,stroke:#0ca678,color:#fff; +``` + +## 5. La CI durcie, une vraie barrière + +![Intégration continue](assets/ci.png){ .logo-md } + +
+ +- 🎯 Prise en compte du besoin une chaîne CI/CD n'a de valeur que si elle est **infranchissable**. + Si un développeur peut contourner un test qui échoue, la barrière ne sert plus à rien. On voulait un + pipeline strict, où un seul contrôle en échec bloque tout le reste. +- 🎓 Vu en cours et bonnes pratiques le **moindre privilège** sur les jetons, l'analyse statique + (**SAST**) avec CodeQL, le principe **fail-fast**, et l'interdiction de tout contournement (le + fameux `continue-on-error`). +- 📋 Contraintes de l'énoncé `permissions: contents: read` global, cache Node, CodeQL + SARIF + avec échec sur `High`/`Error`, tests et Gitleaks bloquants, `continue-on-error` interdit, CD dépendante. +- 🛠️ Ce qu'on met en place des permissions globales en lecture seule (écritures ouvertes + uniquement dans le job concerné), un contrôle `jq` sur le rapport SARIF, et un graphe `needs` strict. +- ⚠️ À ne pas oublier `contents: read` **global**, écritures **isolées**, `continue-on-error` + **interdit**, CodeQL doit **faire échouer** sur une faille majeure, et le **SARIF est téléversé**. + +
+ +```mermaid +flowchart LR + V[validate-workflows]:::info --> T[test]:::info + V --> G[gitleaks]:::info + T --> C[codeql]:::info + T --> S[sbom-scan]:::info + T --> D[docker-image]:::info + G --> D + C --> D + S --> D + D --> DF[deploy-frontend]:::prod + D --> DB[deploy-backend]:::prod + classDef info fill:#1c7ed6,stroke:#1971c2,color:#fff; + classDef prod fill:#0b7285,stroke:#095c6b,color:#fff; +``` + +## 6. La composite action + +![Trivy](assets/trivy.png){ .logo-md } + +
+ +- 🎯 Prise en compte du besoin le scan de dépendances (via un SBOM) pourrait être réutilisé à + plusieurs endroits. Copier-coller la même suite de commandes serait fragile et pénible à maintenir. + On voulait un composant unique, autonome et réutilisable. +- 🎓 Vu en cours et bonnes pratiques le principe **DRY** (« Don't Repeat Yourself »), qui pousse + à factoriser la logique répétée, et la notion de **boîte noire** : une action masque sa complexité + derrière des **entrées** et **sorties** claires. +- 📋 Contraintes de l'énoncé type `composite`, **entrée obligatoire** (le chemin du SBOM + CycloneDX), échec **uniquement** sur `CRITICAL`, et simple **avertissement** pour `HIGH` et `MEDIUM`. +- 🛠️ Ce qu'on met en place un `action.yml` composite, une entrée requise, l'installation de + Trivy intégrée à l'action, et deux niveaux de sévérité distincts. +- ⚠️ À ne pas oublier type **composite** avec entrée **obligatoire**, échec **seulement sur + CRITICAL**, `HIGH`/`MEDIUM` en **avertissement**, et l'action **installe Trivy elle-même**. + +
+ +```mermaid +flowchart LR + sbom[SBOM CycloneDX]:::info --> action[Action trivy-scan]:::info + action --> crit{CRITICAL ?}:::warn + crit -->|oui| fail[Build en échec]:::danger + crit -->|non| high{HIGH / MEDIUM ?}:::warn + high -->|oui| warn[Avertissement dans le résumé]:::warnnode + high -->|non| ok[Rien à signaler]:::ok + warn --> continue[Le pipeline continue]:::ok + classDef info fill:#1c7ed6,stroke:#1971c2,color:#fff; + classDef warn fill:#f59f00,stroke:#e8590c,color:#fff; + classDef warnnode fill:#f08c00,stroke:#e8590c,color:#fff; + classDef danger fill:#e03131,stroke:#c92a2a,color:#fff; + classDef ok fill:#12b886,stroke:#0ca678,color:#fff; +``` + +## 7. Le déploiement continu + +![Déploiement continu](assets/cd.png){ .logo-md } + +
+ +- 🎯 Prise en compte du besoin une fois la CI verte, on veut une mise en production + **automatique** et sans intervention manuelle, mais uniquement pour du code validé, et sans laisser + traîner un secret au passage. +- 🎓 Vu en cours et bonnes pratiques l'**OIDC** (OpenID Connect), qui donne à GitHub une + **identité temporaire** auprès du service cible sans secret longue durée (plus sûr qu'un token + permanent), le déploiement **hermétique** (artefact éphémère), et l'injection des secrets **à la volée**. +- 📋 Contraintes de l'énoncé déploiement **sur `main` uniquement** et seulement si toute la CI + est verte, frontend sur GitHub Pages en **OIDC** (`pages: write` + `id-token: write`), backend sur + Vercel avec secrets déchiffrés en RAM. +- 🛠️ Ce qu'on met en place deux jobs conditionnés à `main` et dépendants de tout, + `upload-pages-artifact` + `deploy-pages` pour le frontend, et `vercel deploy` avec les variables en `--env`. +- ⚠️ À ne pas oublier déploiement sur `main` **uniquement** et CI **verte**, **OIDC** (`pages` + + `id-token`), et secrets **injectés à la volée** sans rien écrire sur le disque. + +
+ +```mermaid +flowchart LR + ci[CI 100% verte sur main]:::ok --> front[deploy-frontend]:::prod + ci --> back[deploy-backend]:::prod + front --> pages[GitHub Pages, OIDC]:::info + back --> vercel[Vercel, secrets en RAM]:::info + vercel --> health{/api/health = 200 ?}:::warn + health -->|non| alert[Job en échec, alerte]:::danger + health -->|oui| done[Déploiement validé]:::ok + classDef ok fill:#12b886,stroke:#0ca678,color:#fff; + classDef prod fill:#0b7285,stroke:#095c6b,color:#fff; + classDef info fill:#1c7ed6,stroke:#1971c2,color:#fff; + classDef warn fill:#f59f00,stroke:#e8590c,color:#fff; + classDef danger fill:#e03131,stroke:#c92a2a,color:#fff; +``` + +## 8. La robustesse + +
+ +- 🎯 Prise en compte du besoin deux risques restaient à couvrir. Gaspiller des ressources (et + créer des conflits de déploiement) quand plusieurs commits arrivent coup sur coup, et déployer une + version cassée sans s'en rendre compte. +- 🎓 Vu en cours et bonnes pratiques l'**annulation de concurrence**, qui stoppe + automatiquement les exécutions devenues inutiles, et le **healthcheck** (smoke test) juste après le + déploiement, une petite requête qui confirme que l'application répond vraiment. +- 📋 Contraintes de l'énoncé si deux commits sont poussés coup sur coup, le pipeline du premier + doit **s'annuler immédiatement**, et une requête `curl` vers `/api/health` doit **faire échouer** le + job si la réponse n'est pas `200`. +- 🛠️ Ce qu'on met en place un bloc `concurrency` avec `cancel-in-progress`, et un `curl --fail` + sur l'URL de production générée dynamiquement par Vercel. +- ⚠️ À ne pas oublier annulation **immédiate** du run périmé, healthcheck sur l'**URL + dynamique**, et tout code de réponse **différent de 200** fait **échouer** le job. + +
+ +```mermaid +flowchart LR + push[Deux push rapprochés]:::warn --> cancel[Le 1er pipeline s'annule]:::info + cancel --> deploy[Déploiement du plus récent]:::prod + deploy --> health{/api/health = 200 ?}:::warn + health -->|oui| ok[Tout va bien]:::ok + health -->|non| alert[Job en échec, alerte équipe]:::danger + classDef warn fill:#f59f00,stroke:#e8590c,color:#fff; + classDef info fill:#1c7ed6,stroke:#1971c2,color:#fff; + classDef prod fill:#0b7285,stroke:#095c6b,color:#fff; + classDef ok fill:#12b886,stroke:#0ca678,color:#fff; + classDef danger fill:#e03131,stroke:#c92a2a,color:#fff; +``` + +Chaque exigence est reprise, **avec sa preuve** (extrait de code, configuration, déploiement en +ligne), sur la page [Conformité](conformite.md). Le détail technique complet se trouve dans la section +[Implémentation](architecture.md). + +
diff --git a/documentation/docs/deploiement.md b/documentation/docs/deploiement.md index f061bd9..e7665d1 100644 --- a/documentation/docs/deploiement.md +++ b/documentation/docs/deploiement.md @@ -1,9 +1,13 @@ # Déploiement continu +![Déploiement continu](assets/cd.png){ .logo } + Le déploiement ne se déclenche **que si toute la CI est verte** et **uniquement sur `main`**. ## Frontend → GitHub Pages +![GitHub Pages](assets/github_pages.png){ .logo } + Publication hermétique via **OIDC**, sans laisser de trace de build dans l'historique Git. ```yaml @@ -30,6 +34,9 @@ deploy-frontend: ## Backend → Vercel +![Node.js](assets/nodejs.png){ .logo } +![Vercel](assets/vercel.png){ .logo } + Déploiement piloté **en ligne de commande** dans GitHub Actions, secrets déchiffrés **en RAM**. ```yaml diff --git a/documentation/docs/gouvernance.md b/documentation/docs/gouvernance.md index c54e4ad..81a9d97 100644 --- a/documentation/docs/gouvernance.md +++ b/documentation/docs/gouvernance.md @@ -1,5 +1,7 @@ # Gouvernance Git +![GitHub](assets/github.png){ .logo } + La politique de sécurité est **lisible directement dans le YAML** : à la simple lecture du workflow, un auditeur constate qu'un déploiement sur `main` exige le succès absolu des jobs amont. diff --git a/documentation/docs/index.md b/documentation/docs/index.md index 748b1da..eec433b 100644 --- a/documentation/docs/index.md +++ b/documentation/docs/index.md @@ -1,64 +1,72 @@ -# SecureWallet +# SecureWallet, une chaîne de livraison sécurisée ! ♾️ -**Chaîne de livraison auditable, hermétique et infalsifiable** pour une SPA statique et une API -Node.js manipulant des données sensibles, industrialisées en un pipeline durci où *aucun code -n'atteint la production sans validation technique*. +![CI/CD sécurisée](assets/ci-cd.webp){ .hero } -!!! note "Projet final — DevSecOps avancé (Sophia Ynov Campus)" - Une équipe de développement nous confie le dépôt d'une application : une SPA statique - (`frontend/`) et une API Node.js / Express (`backend/`) manipulant des données hautement - sensibles (clés d'API, accès à des infrastructures externes). **Notre mission : - industrialiser, durcir et sécuriser toute la chaîne CI/CD** — gouvernance Git, secrets - chiffrés de bout en bout, conteneurisation, analyse statique, déploiement continu. +## Projet final DevSecOps · M1 Cloud, Sécurité & Infrastructure -[:material-web: Frontend (GitHub Pages)](https://sorway.github.io/DevSecOps/){ .md-button .md-button--primary } -[:material-server: API (Vercel)](https://projet-final-inky-iota.vercel.app){ .md-button } +
+ + + Sophia Ynov Campus + + DevSecOps +
+ +**SecureWallet** industrialise une SPA statique et une API **Node.js** ![Node.js](assets/nodejs.png){ .inline-logo } / Express en une **chaîne CI/CD durcie**, auditable et infalsifiable. L'image backend **Docker** ![Docker](assets/docker.png){ .inline-logo } (multi-stage, non-root) est scannée par **Trivy** ![Trivy](assets/trivy.png){ .inline-logo } puis publiée sur **GHCR**. Le code passe au crible de **CodeQL** ![CodeQL](assets/codeql.png){ .inline-logo }, les secrets sont traqués par **Gitleaks** ![Gitleaks](assets/Gitleaks.png){ .inline-logo } et chiffrés par enveloppe avec **SOPS** ![SOPS](assets/sops.png){ .inline-logo } + age. + +Le tout est orchestré par **GitHub Actions** ![GitHub Actions](assets/github_actions.png){ .inline-logo } et déployé sur **GitHub Pages** ![GitHub Pages](assets/github_pages.png){ .inline-logo } (frontend, via OIDC) et **Vercel** ![Vercel](assets/vercel.png){ .inline-logo } (backend). Ce projet conclut un bloc de formation d'environ **28 h** de DevSecOps à Sophia Ynov Campus, en filière Cloud, Sécurité & Infrastructure. + +[:material-web: Frontend](https://sorway.github.io/DevSecOps/){ .md-button .md-button--primary } +[:material-server: API](https://projet-final-inky-iota.vercel.app){ .md-button } [:material-github: Dépôt](https://github.com/Sorway/DevSecOps){ .md-button } -## Les deux composants - -| Composant | Description | Livraison | -|-----------|-------------|-----------| -| `frontend/` | Single Page Application 100 % statique (HTML / CSS / JS moderne) consommant l'API | **GitHub Pages** via OIDC | -| `backend/` | API REST Node.js / Express manipulant des données sensibles | **Docker → GHCR** + **Vercel** | - -## Le pipeline en un coup d'œil - -À la simple lecture de [`ci-cd.yml`](https://github.com/Sorway/DevSecOps/blob/main/.github/workflows/ci-cd.yml), -un auditeur constate qu'un déploiement sur `main` **exige le succès absolu** des jobs de validation amont. - -```mermaid -flowchart LR - subgraph CI["Intégration continue (staging + main)"] - direction LR - A[validate-workflows] --> B[test] - A --> G[gitleaks] - B --> C[codeql] - B --> S[sbom-scan] - B --> D - G --> D - C --> D - S --> D - D[docker-image] - end - subgraph CD["Déploiement (main uniquement)"] - direction LR - D --> F[deploy-frontend] - D --> K[deploy-backend] - end - classDef prod fill:#0b7285,stroke:#0b7285,color:#fff; - class F,K prod; -``` - -## Points clés - -- :material-source-branch: **Gouvernance** : `staging` pivot d'intégration, `main` protégée (revue + status checks, push direct interdit). -- :material-shield-check: **Shift-Left** : hook `pre-commit` (actionlint + gitleaks + refus des `.env`/`.pem`/`.key`). -- :material-lock: **Secrets** : chiffrement par enveloppe SOPS + age, déchiffrés en RAM, jamais sur disque. -- :material-docker: **Conteneur** : Dockerfile multi-stage non-root, scan Trivy, publication conditionnelle sur GHCR taggée au SHA. -- :material-magnify-scan: **CI hermétique** : moindre privilège, cache npm, CodeQL (SARIF), barrière stricte sans `continue-on-error`. -- :material-rocket-launch: **CD** : GitHub Pages (OIDC) + Vercel (secrets injectés à la volée), healthcheck post-déploiement. - -!!! tip "Par où commencer" - Lisez d'abord le [Contexte & consignes](contexte.md), puis suivez la navigation dans l'ordre. - La page [Conformité aux consignes](conformite.md) récapitule, point par point, la couverture de l'énoncé. +## L'Équipe sur ce lab + +
+
+ Thibaut Gianola + Thibaut Gianola + @astronas +
+
+ Jonathan Panzer + Jonathan Panzer + @Sorway +
+
+ Redouane Kachour + Redouane Kachour + @tiwen2 +
+
+ +## De quoi s'agit-il ? + +Quand une entreprise met en ligne une application qui manipule des données sensibles (comptes, clés +d'accès, informations personnelles), le vrai danger n'est pas seulement dans le code lui-même : il +est dans **tout ce qui l'entoure**. Qui a le droit de modifier quoi ? Un mot de passe oublié dans un +fichier, une faille glissée sans le vouloir, une image serveur vulnérable... comment les repère-t-on +**avant** qu'ils n'arrivent chez les utilisateurs ? + +Ce projet construit cette **chaîne de sécurité automatisée** autour d'une petite application web. À +chaque modification du code, une série de contrôles se lance toute seule : recherche de secrets +oubliés, analyse du code à la recherche de failles, scan des vulnérabilités, tests automatiques. Tant +que tout n'est pas au vert, rien n'est mis en ligne, et chaque action reste tracée et vérifiable. + +Cette documentation raconte, étape par étape, **comment** on l'a construite. + +Elle s'adresse à trois publics : + +- Au **correcteur**, pour vérifier point par point que le cahier des charges est respecté ! +- A un **développeur curieux**, pour voir concrètement à quoi ressemble un pipeline + DevSecOps industriel de bout en bout... +- A **nous**, l'équipe, comme mémoire technique du projet. + +## Comment lire cette documentation ? + + +1. **[Contexte & consignes](contexte.md)** : Le travail qui nous a été demandé, l'analyse qu'on en a fait, et les projections faites... +2. **[Implémentation](architecture.md)** : Le détail technique de la production, section par section. +3. **[Conformité](conformite.md)** : La couverture de l'énoncé, exigence par exigence ! + +
diff --git a/documentation/docs/robustesse.md b/documentation/docs/robustesse.md index bfcf00f..4c6fe3e 100644 --- a/documentation/docs/robustesse.md +++ b/documentation/docs/robustesse.md @@ -13,9 +13,8 @@ concurrency: cancel-in-progress: true ``` -!!! note "Sérialisation du déploiement Pages" - Le job `deploy-frontend` a en plus sa propre concurrence (`cancel-in-progress: false`) pour - **ne pas interrompre** un déploiement Pages en cours, tout en n'en lançant qu'un à la fois. +Le job `deploy-frontend` a en plus sa propre concurrence (`cancel-in-progress: false`) pour +**ne pas interrompre** un déploiement Pages en cours, tout en n'en lançant qu'un seul à la fois. ## Healthcheck post-déploiement diff --git a/documentation/docs/sast.md b/documentation/docs/sast.md index a157bf5..c8efa11 100644 --- a/documentation/docs/sast.md +++ b/documentation/docs/sast.md @@ -4,6 +4,8 @@ Deux moteurs d'analyse de sécurité, chacun **bloquant** selon sa criticité. ## CodeQL (analyse statique) +![CodeQL](assets/codeql.png){ .logo } + Le job `codeql` analyse le JavaScript avec le pack `security-extended`, **téléverse le SARIF**, puis **échoue** si une vulnérabilité majeure est détectée. @@ -35,6 +37,9 @@ codeql: ## Trivy (SBOM + image) +![Trivy](assets/trivy.png){ .logo } +![Docker](assets/docker.png){ .logo } + Trivy intervient à deux endroits, avec des politiques distinctes : | Cible | Étape | Politique | diff --git a/documentation/docs/secrets.md b/documentation/docs/secrets.md index 344e6d9..8fa8171 100644 --- a/documentation/docs/secrets.md +++ b/documentation/docs/secrets.md @@ -1,5 +1,7 @@ # Secrets par enveloppe (SOPS / age) +![SOPS](assets/sops.png){ .logo } + La philosophie GitOps exige qu'**aucun secret de production ne soit stocké en clair**, tout en laissant l'équipe Ops **auditer les structures de fichiers**. @@ -27,11 +29,13 @@ sops: encrypted_regex: ^(DATABASE_URL|JWT_SECRET|API_KEY)$ # ← seules ces valeurs ``` -!!! tip "Pourquoi `encrypted_regex`" - Il cible les valeurs à chiffrer par leur clé. Les noms de champs (`DATABASE_URL`…) restent en - clair, donc un reviewer voit *la structure* du fichier évoluer sans jamais voir les secrets. +Le motif `encrypted_regex` cible les valeurs à chiffrer par leur clé. Les noms de champs +(`DATABASE_URL`, `JWT_SECRET`, `API_KEY`) restent en clair : un reviewer voit *la structure* du +fichier évoluer dans les `git diff` sans jamais voir les secrets. + +## Déchiffrement au runtime : 100 % en RAM -## Déchiffrement au runtime — 100 % en RAM +![RAM](assets/ram.png){ .logo } En CD, le secret GitHub `SOPS_AGE_KEY` (la clé privée age) est fourni **en variable d'environnement**. SOPS le lit **nativement en mémoire** ; le fichier est déchiffré en RAM et les valeurs sont injectées diff --git a/documentation/docs/shift-left.md b/documentation/docs/shift-left.md index fadee7c..a8cd800 100644 --- a/documentation/docs/shift-left.md +++ b/documentation/docs/shift-left.md @@ -1,5 +1,7 @@ # Durcissement local (Shift-Left) +![GitHub Actions](assets/github_actions.png){ .logo } + Avant qu'une seule ligne n'atteigne GitHub, le hook **`scripts/pre-commit.sh`** garantit l'intégrité des commits sur le poste de travail. Il agit comme une **barrière locale**. @@ -26,8 +28,8 @@ Le hook est **versionné** (donc auditable et réinstallable). Sur un nouveau cl Le hook **bloque le commit** (`exit ≠ 0`) si l'un des trois échoue : -1. **`actionlint`** sur l'ensemble de `.github/workflows/` — bloque en cas d'erreur de workflow. -2. **`gitleaks`** sur les modifications **indexées uniquement** (`--staged`) — bloque si un secret est trouvé. +1. **`actionlint`** sur l'ensemble de `.github/workflows/` : bloque en cas d'erreur de workflow. +2. **`gitleaks`** sur les modifications **indexées uniquement** (`--staged`) : bloque si un secret est trouvé. 3. **Contrôle de structure strict** : si un fichier `.env` / `.pem` / `.key` est présent dans l'index, le commit est **avorté immédiatement** avec un message rouge explicite. @@ -36,12 +38,13 @@ Le hook **bloque le commit** (`exit ≠ 0`) si l'un des trois échoue : Sécurité : Tentative de commit d'un fichier de configuration ou d'une clé en clair. Opération annulée. ``` -!!! note "Barrière réellement bloquante" - Le hook capture le code de retour d'`actionlint` et de `gitleaks` : un échec **interrompt** le - commit (il n'affiche pas « OK » à tort). Les trois contrôles sont donc de vraies barrières. +Le hook capture le code de retour d'`actionlint` et de `gitleaks` : un échec **interrompt** le +commit (il n'affiche pas « OK » à tort). Les trois contrôles sont donc de vraies barrières. ## Règle Gitleaks sur-mesure +![Gitleaks](assets/Gitleaks.png){ .logo } + Fichier [`gitleaks.toml`](https://github.com/Sorway/DevSecOps/blob/main/gitleaks.toml) : interception des jetons internes de l'entreprise, préfixe `SECWALLET_` suivi d'**exactement 24 caractères alphanumériques majuscules**, combinée à une vérification d'**entropie**. diff --git a/documentation/docs/stylesheets/extra.css b/documentation/docs/stylesheets/extra.css new file mode 100644 index 0000000..922459d --- /dev/null +++ b/documentation/docs/stylesheets/extra.css @@ -0,0 +1,257 @@ +/* ========================================================================= + SecureWallet · styles de la documentation + Diagrammes Mermaid colorés et animés (flux "marching ants"). + ========================================================================= */ + +/* --- Logos sous les titres : grande illustration centrée ------------------ */ +/* On force la hauteur car le CSS responsive du thème (img { height:auto }) + écrase l'attribut height=. La classe + !important reprend la main. */ +.md-typeset p:has(> img.logo), +.md-typeset p:has(> img.logo-md), +.md-typeset p:has(> img.logo-strip) { + text-align: center; +} +.md-typeset img.logo { + height: 256px !important; + width: auto !important; + vertical-align: middle; + margin: 12px; +} + +/* Illustrations de thème (page Contexte) : taille intermédiaire */ +.md-typeset img.logo-md { + height: 150px !important; + width: auto !important; + vertical-align: middle; + margin: 10px; +} + +/* Bandeau de logos en tête de l'accueil (Ynov Campus + DevSecOps) */ +.md-typeset .hero-logos { + display: flex; + flex-wrap: wrap; + align-items: left; + justify-content: left; + gap: 2.2rem; + margin: 1.6rem 0 1.4rem; +} +.md-typeset .hero-logos picture img { + height: 60px !important; + width: auto !important; +} +.md-typeset .hero-logos > img { + height: 84px !important; + width: auto !important; +} + +/* Grande image héros de l'accueil (arrondie, centrée, ombrée) */ +.md-typeset p:has(> img.hero) { + text-align: center; +} +.md-typeset img.hero { + max-width: min(500px, 100%) !important; + height: auto !important; + border-radius: 14px; + box-shadow: 0 10px 34px rgba(0, 0, 0, 0.28); + margin: 1rem auto; +} + +/* Titre de section à barre latérale (style ynov-virtu) */ +.md-typeset .section-bar { + border-left: 5px solid var(--md-primary-fg-color, #1c7ed6); + padding-left: 0.7rem; +} + +/* Petits logos en ligne dans le texte (à côté des noms d'outils) */ +.md-typeset img.inline-logo { + height: 22px !important; + width: auto !important; + vertical-align: middle; + margin: 0 3px; +} + +/* --- "Notre démarche" : liste stylée en stepper coloré -------------------- */ +.md-typeset .demarche > ul { + list-style: none; + margin-left: 0; + padding-left: 0; +} +.md-typeset .demarche > ul > li { + border-left: 4px solid var(--md-primary-fg-color, #1c7ed6); + background: rgba(127, 127, 127, 0.08); + border-radius: 0 8px 8px 0; + padding: 0.6rem 0.9rem; + margin: 0.55rem 0; + transition: transform 0.15s ease, box-shadow 0.15s ease; +} +.md-typeset .demarche > ul > li:hover { + transform: translateX(3px); + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.16); +} +.md-typeset .demarche > ul > li > .twemoji:first-child { + font-size: 1.15em; + margin-right: 0.15rem; +} +.md-typeset .demarche > ul > li:nth-child(1) { border-left-color: #1c7ed6; } +.md-typeset .demarche > ul > li:nth-child(2) { border-left-color: #7048e8; } +.md-typeset .demarche > ul > li:nth-child(3) { border-left-color: #e8590c; } +.md-typeset .demarche > ul > li:nth-child(4) { border-left-color: #12b886; } +.md-typeset .demarche > ul > li:nth-child(5) { border-left-color: #e03131; } + +/* Étiquettes de catégorie : bulles colorées */ +.md-typeset .demarche .cat { + display: inline-block; + color: #fff; + font-size: 0.76em; + font-weight: 700; + padding: 0.14rem 0.7rem; + border-radius: 999px; + margin-right: 0.45rem; + vertical-align: 2px; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); +} +.md-typeset .demarche .cat-1 { background: #1c7ed6; } +.md-typeset .demarche .cat-2 { background: #7048e8; } +.md-typeset .demarche .cat-3 { background: #e8590c; } +.md-typeset .demarche .cat-4 { background: #12b886; } +.md-typeset .demarche .cat-5 { background: #e03131; } + +/* --- Pages sans menu gauche : contenu rapproché de la gauche (sommaire gardé) */ +body:has(.page-noleft) .md-sidebar--primary { display: none !important; } + +/* --- Accueil : contenu aligné sous "Accueil", sans colonnes latérales ------ */ +/* Ciblé uniquement sur la page qui contient le marqueur .page-home. */ +/* On masque la nav gauche (vide) ET le sommaire droit ; le contenu occupe */ +/* alors toute la grille (même largeur que la barre d'onglets), donc aligné. */ +.page-home { display: none; } +body:has(.page-home) .md-sidebar--primary, +body:has(.page-home) .md-sidebar--secondary { + display: none !important; +} +body:has(.page-home) .md-content { + max-width: none !important; +} + +/* --- Bandeau de logos de l'accueil : petits, en ligne, centrés ------------ */ +.md-typeset img.logo-strip { + height: 56px !important; + width: auto !important; + vertical-align: middle; + margin: 6px 10px; +} + +/* --- Zone de contenu élargie (utile sur les pages sans menu latéral) ------ */ +.md-grid { + max-width: 72rem; +} + +/* --- Diagrammes Mermaid plus grands et centrés ---------------------------- */ +.md-typeset .mermaid { + text-align: center; +} +.md-typeset .mermaid svg { + max-width: 100% !important; + width: 100%; + height: auto; +} + +/* --- Section équipe (cartes de membres, style ynov-virtu) ----------------- */ +.md-typeset .team-title { + border-left: 4px solid var(--md-primary-fg-color, #1c7ed6); + padding-left: 0.6rem; +} +.md-typeset .team { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 2.4rem; + margin: 1.6rem 0; +} +.md-typeset .team .member { + width: 140px; + text-align: center; + display: flex; + flex-direction: column; + align-items: center; +} +.md-typeset .team .member img { + width: 92px !important; + height: 92px !important; + border-radius: 50%; + object-fit: cover; +} +.md-typeset .team .member strong { + margin-top: 0.7rem; + line-height: 1.2; +} +.md-typeset .team .member .handle { + color: var(--md-default-fg-color--light); + font-size: 0.82em; +} +.md-typeset .team .m-blue img { box-shadow: 0 0 22px 3px #4dabf7; } +.md-typeset .team .m-purple img { box-shadow: 0 0 22px 3px #b197fc; } +.md-typeset .team .m-green img { box-shadow: 0 0 22px 3px #69db7c; } + +/* --- Flux animé sur les liens des flowcharts ------------------------------ */ +.mermaid .edgePaths path, +.mermaid path.flowchart-link, +.mermaid .edgePath .path { + stroke-dasharray: 8 5 !important; + animation: sw-flow 850ms linear infinite; +} + +@keyframes sw-flow { + to { stroke-dashoffset: -26; } +} + +/* Pointes de flèche plus lisibles */ +.mermaid marker path, +.mermaid defs marker path { + fill: var(--md-accent-fg-color, #22b8cf) !important; +} + +/* Les liens des state/sequence diagrams pulsent doucement */ +.mermaid .transition, +.mermaid .messageLine0, +.mermaid .messageLine1 { + stroke-dasharray: 6 4 !important; + animation: sw-flow 1100ms linear infinite; +} + +/* --- Nœuds : coins arrondis et ombre douce -------------------------------- */ +.mermaid .node rect, +.mermaid .node polygon, +.mermaid .node circle { + rx: 6px; + ry: 6px; + filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.18)); +} + +/* Sous-graphes plus discrets */ +.mermaid .cluster rect { + rx: 10px; + ry: 10px; + fill-opacity: 0.06 !important; +} + +/* Apparition en fondu du diagramme une fois rendu */ +.mermaid > svg { + animation: sw-fade 500ms ease both; +} +@keyframes sw-fade { + from { opacity: 0; transform: translateY(6px); } + to { opacity: 1; transform: translateY(0); } +} + +/* --- Accessibilité : couper les animations si l'utilisateur le demande ---- */ +@media (prefers-reduced-motion: reduce) { + .mermaid .edgePaths path, + .mermaid path.flowchart-link, + .mermaid .edgePath .path, + .mermaid .transition, + .mermaid .messageLine0, + .mermaid .messageLine1, + .mermaid > svg { + animation: none !important; + } +} diff --git a/documentation/zensical.toml b/documentation/zensical.toml index 39b7eb6..9cbacd9 100644 --- a/documentation/zensical.toml +++ b/documentation/zensical.toml @@ -6,33 +6,43 @@ [project] site_name = "SecureWallet · Documentation DevSecOps" -site_description = "Industrialisation, durcissement et architecture CI/CD d'une SPA statique et d'une API Node.js manipulant des donnees sensibles. Projet final DevSecOps, Sophia Ynov Campus." -site_author = "Equipe SecureWallet · Sophia Ynov Campus" +site_description = "Industrialisation, durcissement et architecture CI/CD d'une SPA statique et d'une API Node.js manipulant des données sensibles. Projet final DevSecOps, Sophia Ynov Campus." +site_author = "Équipe SecureWallet · Sophia Ynov Campus" site_url = "https://sorway.github.io/DevSecOps/docs/" repo_url = "https://github.com/Sorway/DevSecOps" repo_name = "Sorway/DevSecOps" edit_uri = "edit/main/documentation/docs/" -copyright = "Copyright © 2026 Equipe SecureWallet · Sophia Ynov Campus" +copyright = "Copyright © 2026 Équipe SecureWallet · Sophia Ynov Campus" docs_dir = "docs" site_dir = "site" +extra_css = ["stylesheets/extra.css"] + nav = [ { "Accueil" = "index.md" }, { "Contexte & consignes" = "contexte.md" }, - { "Architecture du depot" = "architecture.md" }, - { "Gouvernance Git" = "gouvernance.md" }, - { "Securite" = [ - { "Durcissement local (Shift-Left)" = "shift-left.md" }, - { "Secrets (SOPS / age)" = "secrets.md" }, - { "SAST & scan d'images" = "sast.md" }, + { "Implémentation" = [ + { "Vue d'ensemble" = [ + { "Architecture du dépôt" = "architecture.md" }, + { "Gouvernance Git" = "gouvernance.md" }, + ] }, + { "Sécurité" = [ + { "Durcissement local" = "shift-left.md" }, + { "Secrets (SOPS / age)" = "secrets.md" }, + { "SAST & scan d'images" = "sast.md" }, + ] }, + { "Conteneur & CI" = [ + { "Conteneurisation & GHCR" = "conteneurisation.md" }, + { "Pipeline CI durci" = "ci.md" }, + { "Composite Action Trivy" = "composite-action.md" }, + ] }, + { "Déploiement" = [ + { "Déploiement continu" = "deploiement.md" }, + { "Robustesse & rollback" = "robustesse.md" }, + ] }, ] }, - { "Conteneurisation & GHCR" = "conteneurisation.md" }, - { "Pipeline CI durci" = "ci.md" }, - { "Composite Action Trivy" = "composite-action.md" }, - { "Deploiement continu" = "deploiement.md" }, - { "Robustesse & rollback" = "robustesse.md" }, - { "Conformite aux consignes" = "conformite.md" }, + { "Conformité" = "conformite.md" }, ] [project.theme] @@ -80,7 +90,7 @@ toggle.name = "Basculer en mode clair" [[project.extra.social]] icon = "fontawesome/brands/github" link = "https://github.com/Sorway/DevSecOps" -name = "Depot GitHub du projet" +name = "Dépôt GitHub du projet" [[project.extra.social]] icon = "fontawesome/solid/graduation-cap"