PolicyAsACodeRegoExampleTest
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.regocontient la politique de sécurité.s3_test.regocontient 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.
Backlinks: