Rego 103 : types de valeurs et de règles
16 novembre 2023
0 minutes de lectureCette série d’articles de blog propose une introduction accessible à Rego, le langage de politiques créé par les concepteurs du moteur Open Policy Agent (OPA). Vous débutez et souhaitez vous lancer dans l’écriture de politiques sous forme de code avec Rego ? Vous êtes au bon endroit.
Dans cette série en trois parties, nous allons aborder les sujets suivants :
Partie 1 : Rego 101 : introduction à Rego
Partie 2 : Rego 102 : combiner des requêtes avec AND/OR et des messages personnalisés
Partie 3 (la présente partie !) : types de valeurs et de règles
Pour rappel, Rego est un langage de requête déclaratif créé par les concepteurs du framework Open Policy Agent (OPA). La Cloud Native Computing Foundation (CNCF) a accepté OPA comme projet hébergé au niveau incubateur en avril 2019, puis OPA a quitté le statut d’incubation en 2021.
Rego permet d’écrire des politiques sous forme de code, en appliquant des pratiques de programmation telles que le contrôle de version et la conception modulaire à l’évaluation des ressources cloud et d’infrastructure en tant que code (IaC). OPA est le moteur qui évalue les politiques sous forme de code écrites en Rego. Snyk utilise le langage Rego pour créer des règles personnalisées.
Récapitulatif de la partie 2
Dans la partie 2, nous vous avons montré comment utiliser :
les règles AND et OR
le mot-clé
defaultle mot-clé
notles messages
denypersonnalisés
Dans cette partie, nous allons conclure la série en nous intéressant aux règles d’ensemble, aux règles d’objet, aux fonctions et à l’itération.
Types de valeurs
Dans Rego, une valeur représente un type de données. Chaque valeur appartient à un type spécifique. Les types Rego se répartissent en deux catégories : scalaires et composites.
Les valeurs scalaires représentent une seule unité de données et comprennent les types suivants :
Les chaînes de caractères sont entourées de guillemets doubles.
Les nombres comprennent les entiers positifs et négatifs ainsi que les nombres décimaux.
Les booléens ne peuvent prendre que la valeur
trueoufalse.nullreprésente l’absence de valeur.
Les valeurs composites représentent une collection de valeurs et comprennent les types suivants :
Les tableaux
Les objets
Les ensembles
Si vous avez suivi notre série d’articles de blog, vous avez déjà vu plusieurs exemples de valeurs scalaires et composites. Les valeurs composites étant un peu plus complexes, nous allons prendre le temps de les examiner en détail.
Valeurs composites
Les tableaux sont des listes ordonnées d’une ou plusieurs valeurs, délimitées par des crochets. Un tableau peut contenir des chaînes de caractères, des nombres, d’autres tableaux, des valeurs de types différents, etc. :
["alice", "bob", "carlotta"][2, -5, 3.8][[1, 2, 3], [4, 5, 6]][true, "banana", 17]
Vous pouvez accéder à n’importe quel élément d’un tableau en indiquant son index, c’est-à-dire sa position dans le tableau. Les index commencent toujours à 0 : le premier élément a donc l’index 0, le deuxième l’index 1, et ainsi de suite, comme le montre le tableau users ci-dessous :
Pour récupérer le premier élément de la liste users, utilisez la syntaxe suivante :
Pour affecter le deuxième élément à la variable admin, référez-vous-y ainsi :
Si vous utilisez la syntaxe users[3] pour essayer de récupérer un quatrième élément (inexistant) de la liste, OPA ne trouvera aucune correspondance et renverra un ensemble vide {} (valeur indéfinie).
Les objets sont des listes non ordonnées d’une ou plusieurs paires clé-valeur, délimitées par des accolades. Une clé et une valeur peuvent être de n’importe quel type, et leurs types ne doivent pas nécessairement être identiques. Chaque clé et la valeur correspondante sont séparées par deux-points :
{ "alice": "admin", "bob": "user", "carlotta": "user" }{ "ports": [80, 443] }{ 80: true }
Vous pouvez accéder à la valeur d’un objet en indiquant sa clé. Par exemple, l’objet users ci-dessous contient trois paires clé-valeur :
Dans une requête, pour récupérer la valeur de la paire clé-valeur dont la clé est "alice", utilisez cette syntaxe :
Les ensembles sont des listes non ordonnées d’une ou plusieurs valeurs uniques, également délimitées par des accolades. Les valeurs peuvent être de n’importe quel type :
{1, 2, 3}{"alice", "bob", "carlotta"}
Comme les ensembles ne sont pas ordonnés, deux ensembles contenant les mêmes éléments sont égaux, même si ces éléments apparaissent dans un ordre différent. Par exemple, {1, 2, 3} est égal à {2, 3, 1}.
Vous pouvez vérifier si un élément fait partie d’un ensemble. Supposons que vous disposiez de l’ensemble suivant :
Dans une requête, pour vérifier si l’entier 4 appartient à l’ensemble nums, utilisez cette syntaxe :
Types de règles
Rego comporte plusieurs types de règles :
Les règles complètes, qui produisent un seul résultat.
Les règles qui génèrent des ensembles.
Les règles qui génèrent des objets.
Les fonctions, qui sont en réalité un peu différentes des règles.
Chacune de ces règles peut avoir un corps composé de requêtes. Ce qui les différencie, c’est la façon dont les requêtes sont construites et les informations qu’elles renvoient. Dans la section suivante, nous expliquerons chaque type de règle, en commençant par les règles complètes.
Règles complètes
Si vous avez suivi cette série, vous avez déjà vu une règle complète :
Une règle complète affecte une valeur unique à une variable. Dans l’exemple ci-dessus, la variable allow reçoit la valeur true si la condition de la requête est remplie.
Voici un autre exemple : cette fois, le nombre 80 est affecté à la variable allowed_port si la condition de la requête est remplie :
Une règle complète peut contenir une seule requête, comme ci-dessus, ou plusieurs, comme dans un autre exemple de la partie 2 :
Constantes
Que faire si vous voulez qu’une variable conserve une valeur donnée quelles que soient les circonstances ? Dans d’autres langages de programmation, on parle de constante.
Vous pourriez l’écrire ainsi :
Comme la condition de la requête est toujours remplie, elle renvoie toujours true, quelles que soient les circonstances : la variable pi a toujours la valeur 3.14.
Grâce à une simplification syntaxique, vous pouvez ensuite omettre entièrement le corps :
L’exemple ci-dessus est une règle complète. Où que vous utilisiez pi dans le programme, cette variable représente toujours 3.14.
Compréhensions d’ensembles et d’objets
Dans de nombreux cas, vous pouvez vouloir affecter à une variable une collection de valeurs. C’est là que les compréhensions d’ensembles et les compréhensions d’objets entrent en jeu.
Compréhensions d’ensembles
Une compréhension d’ensemble ajoute des éléments à une variable un par un pour produire un ensemble. Vous pouvez parfois vouloir parcourir le fichier d’entrée pour générer un ensemble contenant toutes les valeurs qui remplissent certaines conditions. Une compréhension d’ensemble vous permet de le faire.
Supposons que vous ayez le document d’entrée suivant, qui représente un tableau de tous les utilisateurs actuellement connectés au système :
Vous vous souvenez de la politique de l’entreprise selon laquelle Alice est la seule à disposer de droits d’administration ? Imaginons que vous vouliez créer une liste unique (un ensemble) de tous les utilisateurs non-administrateurs connectés.
Vous pourriez générer un ensemble comme celui-ci :
Pour expliquer cet exemple, nous allons d’abord examiner le corps de la règle, puis sa tête.
Corps de la règle : il demande à OPA de parcourir le tableau input.users pour trouver les éléments qui ne correspondent pas à "alice".
Comment cela fonctionne-t-il ? Nous avons vu plus haut qu’on accède à un élément d’un tableau à l’aide de son index. Ici, nous utilisons la variable i pour représenter l’index (vous pouvez lui donner un autre nom si vous le souhaitez), car elle est incrémentée à chaque parcours de l’entrée. Lorsqu’OPA exécute les requêtes, il parcourt la liste users en remplaçant i par l’index de chaque élément afin de récupérer les éléments un par un. Si vous connaissez la programmation impérative, le principe ressemble à celui d’une boucle impérative, même s’il n’est pas tout à fait identique.
Lors du premier parcours du tableau users dans l’entrée, voici ce qui se passe en coulisses :
Lors du deuxième parcours, voici ce qui se passe :
Lors du troisième parcours, voici ce qui se passe :
Tête de la règle : la variable nonadmins désigne l’ensemble lui-même, tandis que la variable name représente chaque nom unique de l’ensemble (qui, dans le corps, correspond à input.users[i], comme expliqué ci-dessus). Lorsque OPA suit la logique du corps, si la valeur actuelle de name remplit toutes les conditions, cette valeur est ajoutée à l’ensemble nonadmins.
Récapitulatif : à chaque parcours de la liste des utilisateurs, OPA ajoute la valeur actuelle de input.users[i] à l’ensemble nonadmins[name] si elle remplit toutes les conditions énoncées dans les requêtes.
Voici l’ensemble nonadmins obtenu :
On voit que Bob et Carlotta sont les utilisateurs non-administrateurs de garde !
Itération
À noter à propos de l’itération dans Rego : elle est implicite. Contrairement à d’autres langages comme Python, il n’y a pas de boucles du type « while x == true » ou « for y in z ». Pour parcourir un tableau, un ensemble ou un objet, vous utilisez plutôt une variable à la place d’un index de tableau, d’un élément d’ensemble ou d’une clé d’objet, comme nous le faisons avec i dans le tableau input.users ci-dessous :
Comme nous avons placé une variable entre crochets, OPA sait que nous ne faisons pas référence à une valeur unique, mais que nous les examinons toutes, une par une.
Cela demande un peu d’adaptation si vous avez l’habitude des boucles impératives, mais la syntaxe est concise ! L’exemple ci-dessus équivaut à l’expression Python suivante :
Ou, si vous préférez :
L’opérateur souligné
Si vous n’avez besoin de faire référence à l’index qu’une seule fois, vous pouvez utiliser l’opérateur générique (un souligné) au lieu d’une variable nommée comme i :
Utilisé comme itérateur (à l’instar de la variable i), l’opérateur générique représente n’importe quelle valeur d’un tableau, n’importe quel élément d’un ensemble ou n’importe quelle clé d’un objet. En pratique, le souligné est une variable sans nom, destinée à être ignorée. Dans la règle ci-dessus, OPA vérifie si un nom quelconque du tableau input.users est différent de "alice" (et, si c’est le cas, l’ajoute à l’ensemble des non-administrateurs). Le résultat est exactement le même que si vous aviez utilisé name := input.users[i].
En revanche, si vous devez conserver l’index dans une règle, utilisez une variable nommée, comme ci-dessous :
Avec le document d’entrée suivant…
… vous obtiendriez le résultat suivant :
Dans ce cas, nous devons utiliser une variable nommée, car nous voulons nous assurer que le nom vérifié dans la requête input.users[i] != "alice" est le même que celui affiché dans la requête msg. Il est donc important de conserver l’index. Pour mieux comprendre, regardons ce qu’OPA fait en coulisses lorsqu’il remplace la variable i par un index. Voici un exemple d’itération dans le tableau input.users :
Nous devons utiliser input.users[2] dans les deux requêtes pour nous assurer de faire référence à la valeur du même index (dans ce cas, "carlotta").
Compréhension d’objet
Une compréhension d’objet ajoute des éléments à une variable un par un pour produire un objet, de la même façon que les règles d’ensemble génèrent des ensembles. Toutefois, alors qu’une règle d’ensemble sert à créer une collection de valeurs uniques, une règle d’objet vise à produire une collection de paires clé-valeur.
Le processus ressemble beaucoup à l’écriture d’une règle d’ensemble. Cependant, dans une règle d’objet, vous indiquez la partie valeur de la paire clé-valeur dans la tête de la règle :
Cet exemple est exactement identique à notre premier exemple de règle d’ensemble, sauf qu’ici, la valeur de chaque paire clé-valeur est "logged-in".
Utilisons le même document d’entrée :
Lorsque nous évaluons la règle par rapport à l’entrée ci-dessus, nous obtenons le résultat suivant :
Le résultat est un objet nonadmins contenant deux paires clé-valeur. Chaque paire a pour clé un nom d’utilisateur et pour valeur "logged-in".
Fonctions
Les fonctions Rego ressemblent aux fonctions des autres langages : elles offrent un moyen modulaire et réutilisable de demander au programme de faire quelque chose. Leur syntaxe est semblable à celle des règles et elles déclarent toutes deux des requêtes de la même manière. Toutefois, une fonction inclut un paramètre (qui sert d’espace réservé pour un argument réel) entre parenthèses.
Par exemple, la fonction ci-dessous prend la valeur de x, la double et attribue le résultat à y.
Ailleurs dans le package, vous pouvez l’appeler en lui fournissant un argument sur lequel agir, comme ceci :
Et z prendrait la valeur 4.
Vous pourriez l’appeler à nouveau en lui passant l’argument 12 et en attribuant le résultat à foo ; foo prendrait alors la valeur 24 :
Vous pouvez utiliser une fonction lorsqu’une tâche très spécifique doit être effectuée plusieurs fois, en particulier au sein d’autres fonctions. C’est pratique pour obtenir un code plus clair et plus modulaire. Si vous devez répéter une tâche avec différentes entrées, vous pouvez écrire une fonction pour l’accomplir.
Vous pouvez également écrire des fonctions « utilitaires » utilisées dans d’autres fonctions. Ci-dessous, la variable allow prend la valeur true si tous les éléments du tableau input.tags sont valides. Pour vérifier si un élément est valide, les fonctions utilitaires is_lowercase_value et is_long_enough vérifient respectivement si une chaîne passée en argument est en minuscules ou si elle a la longueur requise. Elles sont toutes deux utilisées dans la fonction is_valid, appelée depuis allow :
Évaluer des exemples de règles avec OPA
Expérimentons avec les règles d’ensembles, les règles d’objets et les fonctions en Rego. Comme dans les précédents articles de blog, nous allons nous concentrer sur deux façons d’interagir avec OPA :
Utiliser OPA Playground
Utiliser l’outil de ligne de commande d’OPA
Pour savoir comment utiliser ces interfaces, consultez la partie 1.
Une fois encore, nous allons utiliser un exemple plus proche du monde réel, avec un pod Kubernetes. Voici le manifeste JSON que nous utiliserons comme entrée :
Et voici la stratégie que nous allons évaluer, qui applique l’exigence de l’entreprise suivante : « Les conteneurs des pods en environnement de production ne doivent pas utiliser l’image la plus récente » :
Cet ensemble de règles illustre plusieurs concepts abordés dans cet article, comme les fonctions, les règles d’ensembles, l’itération et l’opérateur de soulignement. Voici comment tout cela fonctionne :
is_labeled_prod(labels) — Il s’agit d’une fonction qui renvoie true si l’ensemble des étiquettes qui lui sont transmises contient une étiquette "stage" dont la valeur est "prod" ou "production".
latest_containers[container] — Il s’agit d’une règle d’ensemble. OPA utilise l’opérateur de soulignement comme itérateur pour vérifier si l’un des conteneurs en entrée utilise l’image la plus récente. Si c’est le cas, il l’ajoute à l’ensemble latest_containers. Pour déterminer si une image est la plus récente, nous vérifions si son nom se termine par ":latest".
deny[msg] — Il s’agit également d’une règle d’ensemble ! C’est une version plus avancée de la règle d’ensemble deny[msg] présentée dans la partie 2. Elle comporte trois requêtes auxquelles OPA applique les actions suivantes :
Vérifier si
input.kindest égal à « Pod ».Appeler la fonction
is_labeled_proden lui transmettantinput.metadata.labelspour vérifier s’il existe des étiquettes"stage"dont la valeur est"prod"ou"production".Parcourir l’ensemble
latest_containerspour vérifier s’il contient au moins un conteneur.
SI les trois requêtes ci-dessus renvoient true (c’est-à-dire si OPA trouve une correspondance dans l’entrée pour chaque condition), ALORS OPA ajoute à l’ensemble deny un message personnalisé indiquant le nom du conteneur non conforme.
Pour vous faciliter la tâche, nous avons créé un espace de test contenant déjà cet exemple : https://play.openpolicyagent.org/p/HgHE4w2b4y
Si vous évaluez les règles en cliquant sur le bouton Evaluate dans l’espace de test, ou en exécutant une commande telle que opa eval -i input.json -d check_prod_pod.rego "data.rules.check_prod_pod" --format pretty si vous exécutez OPA en local, vous verrez le résultat suivant :
Le premier élément du résultat est l’ensemble deny, qui indique que le conteneur myapp1 n’est pas conforme à notre stratégie. Vous pouvez également voir les éléments de l’ensemble latest_containers, qui contient le nom et l’image de chaque conteneur — dans cet exemple, le seul conteneur est myapp1.
Voyons ce qui se passe si nous modifions le nom de l’image de myapp2 en myapp2:latest (ligne 19). Si nous évaluons à nouveau les règles, l’ensemble deny contient désormais un message pour myapp1 et myapp2, et l’ensemble latest_containers contient les deux conteneurs :
Enfin, supprimons la ligne "stage": "prod" (ligne 7) pour que l’entrée ressemble à ceci :
Si nous évaluons les règles maintenant, nous constatons que latest_containers contient toujours les deux conteneurs, mais que l’ensemble deny est vide :
Comme le pod n’est pas étiqueté comme étant en environnement de production, ses conteneurs peuvent utiliser l’image la plus récente conformément à la stratégie de l’entreprise. Le pod est donc conforme !
Et maintenant ?
Félicitations ! Vous êtes arrivé au terme de notre série de trois articles consacrés à l’écriture de Rego.
Pour approfondir le sujet, voici quelques ressources utiles :
Si vous souhaitez utiliser Rego pour écrire des règles personnalisées pour Snyk IaC, consultez notre documentation. En plus des ensembles de règles intégrés de Snyk, dédiés à la sécurité et associés aux exigences de conformité, les règles personnalisées IaC+ vous permettent de définir des contrôles de sécurité adaptés à toutes les étapes de votre SDLC.
IaC+ vous offre une vue unifiée de vos problèmes de configuration, du code au cloud, ainsi que les contrôles nécessaires grâce à une interface dédiée aux problèmes, un ensemble de règles et un moteur de stratégies couvrant les IDE, SCM, CLI, CI/CD, Terraform Cloud et les environnements cloud déployés tels qu’AWS, Azure et Google Cloud.
Lancez-vous dans les compétitions Capture The Flag
Apprenez à résoudre des défis Capture The Flag en regardant à la demande notre atelier virtuel d’initiation.



