Aller au contenu

Test Rego

Pour tester cette règle, il faut lui fournir plusieurs jeux de données représentatifs : configurations non conformes, conformes et limites. Avec OPA, les tests sont eux-mêmes des règles Rego dont le nom commence par test_, puis on les exécute avec opa test. openpolicyagent

Organisation recommandée

Placez la règle et les tests côte à côte dans un dépôt Git :

policies/
├── s3.rego
└── s3_test.rego
  • s3.rego contient la politique de sécurité.
  • s3_test.rego contient les jeux de données de test et les résultats attendus.
  • La CI exécute opa test policies/ -v à chaque pull request.

OPA charge les fichiers transmis, détecte les règles préfixées par test_, puis retourne un succès ou un échec pour chacune. Il est également capable de produire des données de couverture de test. openpolicyagent

Politique à tester

Voici une version légèrement structurée de la règle précédente, dans policies/s3.rego :

package terraform.security

default deny := []

# Interdit explicitement une ACL qui rend un bucket publiquement lisible.
deny contains message if {
  resource := input.resource_changes[_]

  resource.type == "aws_s3_bucket"
  resource.change.after.acl == "public-read"

  message := sprintf(
    "Refus : le bucket %q est public (ACL public-read).",
    [resource.address],
  )
}

# Interdit la désactivation du blocage des ACL publiques.
deny contains message if {
  resource := input.resource_changes[_]

  resource.type == "aws_s3_bucket_public_access_block"
  resource.change.after.block_public_acls == false

  message := sprintf(
    "Refus : %q doit bloquer les ACL publiques.",
    [resource.address],
  )
}

La décision recherchée est data.terraform.security.deny : une liste vide signifie que le plan ne viole pas cette politique ; une liste non vide signifie que le déploiement doit être bloqué.

Tests unitaires Rego

Créez le fichier policies/s3_test.rego :

package terraform.security

# 1. Cas non conforme : bucket explicitement public.
test_bucket_public_est_refuse if {
  refus := deny with input as {
    "resource_changes": [
      {
        "address": "aws_s3_bucket.documents_clients",
        "type": "aws_s3_bucket",
        "change": {
          "after": {
            "bucket": "documents-clients-production",
            "acl": "public-read",
          },
        },
      },
    ],
  }

  count(refus) == 1
  refus[0] == "Refus : le bucket \"aws_s3_bucket.documents_clients\" est public (ACL public-read)."
}

# 2. Cas non conforme : le garde-fou contre les ACL publiques est coupé.
test_blocage_acl_publiques_desactive_est_refuse if {
  refus := deny with input as {
    "resource_changes": [
      {
        "address": "aws_s3_bucket_public_access_block.documents_clients",
        "type": "aws_s3_bucket_public_access_block",
        "change": {
          "after": {
            "block_public_acls": false,
          },
        },
      },
    ],
  }

  count(refus) == 1
  refus[0] == "Refus : \"aws_s3_bucket_public_access_block.documents_clients\" doit bloquer les ACL publiques."
}

# 3. Cas conforme : bucket privé, blocage des ACL publiques activé.
test_bucket_prive_est_accepte if {
  refus := deny with input as {
    "resource_changes": [
      {
        "address": "aws_s3_bucket.documents_clients",
        "type": "aws_s3_bucket",
        "change": {
          "after": {
            "bucket": "documents-clients-production",
            "acl": "private",
          },
        },
      },
      {
        "address": "aws_s3_bucket_public_access_block.documents_clients",
        "type": "aws_s3_bucket_public_access_block",
        "change": {
          "after": {
            "block_public_acls": true,
          },
        },
      },
    ],
  }

  count(refus) == 0
}

# 4. Cas limite : deux défauts doivent être remontés, pas seulement le premier.
test_plusieurs_violations_sont_toutes_retournees if {
  refus := deny with input as {
    "resource_changes": [
      {
        "address": "aws_s3_bucket.documents_clients",
        "type": "aws_s3_bucket",
        "change": {
          "after": {
            "acl": "public-read",
          },
        },
      },
      {
        "address": "aws_s3_bucket_public_access_block.documents_clients",
        "type": "aws_s3_bucket_public_access_block",
        "change": {
          "after": {
            "block_public_acls": false,
          },
        },
      },
    ],
  }

  count(refus) == 2
}

# 5. Non-régression : une ressource qui n'est pas un bucket S3 est ignorée.
test_une_ressource_non_s3_n_est_pas_refusee if {
  refus := deny with input as {
    "resource_changes": [
      {
        "address": "aws_instance.application",
        "type": "aws_instance",
        "change": {
          "after": {
            "instance_type": "t3.small",
          },
        },
      },
    ],
  }

  count(refus) == 0
}

Le mot-clé with input as {...} remplace temporairement l’entrée réelle de la politique par un jeu de données contrôlé. C’est l’approche recommandée par OPA pour tester la même règle avec différents scénarios. openpolicyagent

Exécuter les tests

Installez le binaire OPA puis, depuis la racine du projet :

opa test policies/ -v

Résultat attendu :

policies/s3_test.rego:
data.terraform.security.test_bucket_public_est_refuse: PASS
data.terraform.security.test_blocage_acl_publiques_desactive_est_refuse: PASS
data.terraform.security.test_bucket_prive_est_accepte: PASS
data.terraform.security.test_plusieurs_violations_sont_toutes_retournees: PASS
data.terraform.security.test_une_ressource_non_s3_n_est_pas_refusee: PASS
--------------------------------------------------------------------------------
PASS: 5/5

Pour vérifier que chaque partie de la politique est réellement exécutée par les tests :

opa test policies/ --coverage --format=json

La couverture est utile pour détecter une règle ajoutée mais jamais validée — par exemple une future règle de chiffrement qui ne serait jamais déclenchée par un jeu d’essai. OPA expose ce mécanisme de test et de couverture nativement. openpolicyagent

Tester avec des fichiers JSON

Les tests unitaires ci-dessus sont idéaux pour la CI car ils sont courts et précis. Vous pouvez aussi conserver des fichiers JSON réalistes, issus de vrais plans Terraform anonymisés.

fixtures/
├── bucket-public.json
├── bucket-prive.json
└── protections-desactivees.json

Par exemple fixtures/bucket-public.json :

{
  "resource_changes": [
    {
      "address": "aws_s3_bucket.documents_clients",
      "type": "aws_s3_bucket",
      "change": {
        "after": {
          "bucket": "documents-clients-production",
          "acl": "public-read"
        }
      }
    }
  ]
}

Évaluez ensuite manuellement la politique :

opa eval \
  --data policies/s3.rego \
  --input fixtures/bucket-public.json \
  'data.terraform.security.deny'

Vous devez obtenir le message de refus. Avec bucket-prive.json, la sortie doit être une liste vide.

Jeux à prévoir

Pour chaque règle PaC, prévoyez au minimum ces catégories :

Jeu de données Objectif Résultat attendu
Non conforme simple Vérifier que la violation principale est détectée Refus
Conforme Vérifier qu’un cas valide n’est pas bloqué Autorisation, liste deny vide
Plusieurs violations Vérifier que tous les défauts sont signalés Plusieurs refus détaillés
Cas limite Gérer les champs absents, valeurs nulles, ressources inconnues ou ressources supprimées Comportement explicitement décidé
Régression Reproduire un incident ou un contournement déjà rencontré Refus permanent après correction
Compatibilité Vérifier qu’une règle nouvelle ne bloque pas involontairement une configuration légitime existante Autorisation ou exception contrôlée

L’objectif n’est pas seulement de vérifier que la règle « bloque quelque chose », mais de démontrer qu’elle bloque exactement ce qui doit l’être, tout en laissant passer les déploiements légitimes.