Aller au contenu

Lancer un scan de conformité CIS/STIG sur une machine

Repod note les machines de votre inventaire de parc par rapport à des référentiels de sécurité publiés (CIS, DISA STIG). Ce guide couvre l'assignation d'un profil à des machines et le lancement d'un scan. Pour le modèle sous-jacent — comment se résout l'assignation de profil, pourquoi le profil CIS intégré s'exécute toujours, et en quoi cela diffère de la dérive de configuration — voir Profils de conformité et dérive de configuration.


1. Prérequis

  • La machine est déjà enregistrée dans l'inventaire de parc avec connection_type: ssh. Les scans de conformité nécessitent une connexion SSH — les clients en mode agent ne peuvent pas être scannés (POST /clients/{id}/compliance/scan renvoie 400 pour eux).
  • La lecture des profils/résultats nécessite le rôle auditor, maintainer ou admin. L'assignation d'un profil à un tag ou un client nécessite admin.
  • Le déclenchement d'un scan nécessite un rôle authentifié disposant d'un accès machine sur ce client, et le replica backend doit actuellement être le leader HA (le POST renvoie 503 sur un replica passif dans un déploiement multi-replica — voir require_leader dans les notes d'architecture backend).
  • Si vous prévoyez d'utiliser un profil STIG (ou tout autre profil non intégré), il doit déjà avoir été importé — voir étape 2.

Le profil CIS intégré s'exécute toujours

Chaque scan évalue le profil CIS intégré (cis-builtin-linux) même si une machine n'a aucune assignation de profil — il n'existe pas d'état « aucune couverture de conformité ». Assigner STIG ou un autre profil importé s'ajoute à cela, cela ne remplace jamais le contrôle CIS.


2. Importer un profil (optionnel, STIG uniquement)

Le contenu des référentiels de conformité — scripts de contrôle, sévérités, titres, références officielles — n'est jamais écrit à la main ni approximé de mémoire. Il est uniquement chargé depuis un document de référentiel réellement publié, via backend/scripts/import_compliance_profile.py. Il n'existe aucun chemin dans l'interface pour rédiger du contenu de contrôle.

docker compose exec backend-api python3 scripts/import_compliance_profile.py \
    data/compliance/stig_rhel9_disa.json \
    stig-rhel9-disa stig "DISA STIG — Red Hat Enterprise Linux 9" \
    --os-match rhel9 \
    --source-ref "RedHatOfficial/ansible-role-rhel9-stig (GitHub)"

Le script est idempotent : le relancer avec un fichier JSON mis à jour rafraîchit les métadonnées de contrôle (titre, sévérité, catégorie, référence) mais ne remplace jamais un check_script déjà écrit — un contrôle validé par un humain n'est jamais remplacé silencieusement par un rafraîchissement de métadonnées. Les contrôles sans check_script dans le fichier source sont quand même importés et rapportés not_implemented lors d'un scan, plutôt que d'être abandonnés silencieusement.

Profil 'stig-rhel9-disa' : 412 créé(s), 0 mis à jour, 412 au total.

Lister les profils désormais disponibles :

curl -s http://localhost:8000/api/v1/compliance/profiles \
  -H "Authorization: Bearer $TOKEN" | jq .
{
  "profiles": [
    { "id": "cis-builtin-linux", "framework": "cis", "name": "CIS Benchmark (built-in)", "os_match": null, "is_builtin": true },
    { "id": "stig-rhel9-disa", "framework": "stig", "name": "DISA STIG — Red Hat Enterprise Linux 9", "os_match": "rhel9", "is_builtin": false }
  ]
}

3. Assigner un profil à des machines

L'assignation se fait par tag ou par client (une machine spécifique). Une assignation au niveau client est une surcharge : elle remplace entièrement les assignations dérivées des tags de cette machine, au lieu de s'y ajouter. En l'absence de surcharge, les assignations de chaque tag correspondant s'appliquent ensemble. C'est la même convention « surcharge remplace, ne fusionne jamais » utilisée dans tout le RBAC de parc de Repod — voir Profils de conformité et dérive de configuration pour les règles de résolution complètes.

Assigner à toutes les machines taguées rhel9-prod :

curl -s -X POST http://localhost:8000/api/v1/compliance/profiles/stig-rhel9-disa/assignments \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"principal_type": "tag", "principal_id": "rhel9-prod"}' | jq .

Assigner à un client spécifique, en surchargeant ses assignations dérivées des tags :

curl -s -X POST http://localhost:8000/api/v1/compliance/profiles/stig-rhel9-disa/assignments \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"principal_type": "client", "principal_id": "3f2b1a90-..."}' | jq .

Réponse (201) :

{
  "assignment": {
    "id": "a1b2c3d4-...",
    "profile_id": "stig-rhel9-disa",
    "principal_type": "tag",
    "principal_id": "rhel9-prod",
    "assigned_by": "admin",
    "assigned_at": "2026-08-20T09:00:00Z"
  }
}

Lister les assignations actuelles d'un profil :

curl -s http://localhost:8000/api/v1/compliance/profiles/stig-rhel9-disa/assignments \
  -H "Authorization: Bearer $TOKEN" | jq .

Supprimer une assignation :

curl -s -X DELETE http://localhost:8000/api/v1/compliance/profiles/assignments/a1b2c3d4-... \
  -H "Authorization: Bearer $TOKEN"

4. Déclencher un scan

curl -s -X POST http://localhost:8000/api/v1/inventory/clients/3f2b1a90-.../compliance/scan \
  -H "Authorization: Bearer $TOKEN"
{ "message": "Scan de conformité lancé en arrière-plan", "client_id": "3f2b1a90-..." }

Le scan s'exécute en arrière-plan (202 Accepted) sur une seule connexion SSH, quel que soit le nombre de profils applicables — le script CIS intégré et les scripts de contrôle concaténés de tout profil importé s'exécutent tous sur cette même session.

Forcer un sous-ensemble spécifique de profils au lieu de la résolution automatique par tag/client, en passant profile_ids dans le corps de la requête :

curl -s -X POST http://localhost:8000/api/v1/inventory/clients/3f2b1a90-.../compliance/scan \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"profile_ids": ["cis-builtin-linux", "stig-rhel9-disa"]}'

Scanner toutes les machines correspondant à un tag en un seul appel :

curl -s -X POST http://localhost:8000/api/v1/inventory/compliance/scan-by-tag \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"tags": ["rhel9-prod"], "match": "any"}'

Cela résout tous les clients accessibles connectés en SSH correspondant au(x) tag(s) et déclenche le même scan par client pour chacun, en best-effort — les clients en mode agent présents dans l'ensemble résolu sont silencieusement ignorés (compteur skipped dans la réponse), jamais mis en erreur.


5. Lire les résultats

Vue historique, CIS uniquement (forme de réponse inchangée, comportement historique) :

curl -s http://localhost:8000/api/v1/inventory/clients/3f2b1a90-.../compliance \
  -H "Authorization: Bearer $TOKEN" | jq .
{
  "client_id": "3f2b1a90-...",
  "results": [ { "control_id": "1.1.1.1", "status": "pass", "detail": "..." } ],
  "score": { "passed": 38, "failed": 2, "not_applicable": 1, "percent": 95.0 },
  "scanned_at": "2026-08-20T09:03:41Z"
}

Vue complète multi-profils — chaque profil applicable à cette machine, avec son propre score :

curl -s http://localhost:8000/api/v1/inventory/clients/3f2b1a90-.../compliance/profiles \
  -H "Authorization: Bearer $TOKEN" | jq .
{
  "client_id": "3f2b1a90-...",
  "profiles": [
    {
      "profile_id": "cis-builtin-linux",
      "profile_name": "CIS Benchmark (built-in)",
      "framework": "cis",
      "results": [ "..." ],
      "score": { "passed": 38, "failed": 2, "not_applicable": 1, "percent": 95.0 },
      "scanned_at": "2026-08-20T09:03:41Z"
    },
    {
      "profile_id": "stig-rhel9-disa",
      "profile_name": "DISA STIG — Red Hat Enterprise Linux 9",
      "framework": "stig",
      "results": [ "..." ],
      "score": { "passed": 120, "failed": 5, "not_applicable": 287, "percent": 96.0 },
      "scanned_at": "2026-08-20T09:03:41Z"
    }
  ]
}

Il est normal qu'une grande partie des contrôles d'un profil importé affiche not_applicable : tout contrôle sans check_script écrit est rapporté ainsi plutôt qu'exécuté. Ce n'est pas un échec de scan.


Vérifier que ça a fonctionné

  1. POST .../compliance/scan a renvoyé 202 avec le client_id attendu.
  2. GET .../compliance/profiles affiche un horodatage scanned_at récent pour chaque profil que vous attendez avoir été exécuté (CIS intégré plus tout profil assigné dont l'os_match correspond à la distribution de cette machine — un profil dont l'os_match ne correspond pas est rapporté not_applicable et jamais exécuté, ce qui est également attendu, pas une erreur).
  3. Les logs backend montrent l'achèvement du scan :
    docker compose logs backend-api | grep '\[compliance\] Scan terminé'
    
  4. Pour un profil importé, vérifiez ponctuellement qu'un contrôle dont vous savez qu'il possède un véritable check_script rapporte pass/fail, et non not_implemented — un profil entier bloqué sur not_implemented signifie généralement que le fichier JSON source ne contenait pas encore de valeurs check_script pour lui, pas un bug du scan.