Rego 103: tipos de valores y reglas
16 de noviembre de 2023
0 minutos de lecturaEsta serie de publicaciones ofrece una introducción sencilla a Rego, el lenguaje de políticas de los creadores del motor Open Policy Agent (OPA). Si estás empezando y quieres aprender a escribir políticas como código con Rego, estás en el lugar indicado.
En esta serie de tres partes, veremos lo siguiente:
Parte 1: Rego 101: introducción a Rego
Parte 2: Rego 102: combinar consultas con AND/OR y mensajes personalizados
Parte 3 (¡esta parte!): tipos de valores y reglas
Como recordatorio, Rego es un lenguaje declarativo de consultas de los creadores del framework Open Policy Agent (OPA). La Cloud Native Computing Foundation (CNCF) aceptó OPA como proyecto alojado en etapa de incubación en abril de 2019, y OPA se graduó de esa etapa en 2021.
Rego se usa para escribir políticas como código, lo que aplica prácticas de programación, como el control de versiones y el diseño modular, a la evaluación de recursos de la nube y de infraestructura como código (IaC). OPA es el motor que evalúa las políticas como código escritas en Rego. Snyk usa el lenguaje Rego para las reglas personalizadas.
Resumen de la parte 2
En la parte 2, te mostramos cómo usar lo siguiente:
Reglas AND y OR
La palabra clave
defaultLa palabra clave
notMensajes
denypersonalizados
En esta parte, completaremos la serie centrándonos en las reglas de conjuntos y objetos, las funciones y la iteración.
Tipos de valores
En Rego, un valor representa algún tipo de dato. Cada valor pertenece a un tipo específico. Los tipos de Rego se dividen en dos categorías: escalares y compuestos.
Los valores escalares representan una sola unidad de datos e incluyen los siguientes tipos:
Las cadenas de texto se escriben entre comillas dobles.
Los números incluyen enteros positivos y negativos, y decimales.
Los valores booleanos solo pueden ser
trueofalse.nullrepresenta la ausencia de un valor.
Los valores compuestos representan una colección de valores e incluyen los siguientes tipos:
Arreglos
Objetos
Conjuntos
Si has seguido nuestra serie de publicaciones, ya viste varios ejemplos de valores escalares y compuestos. Los valores compuestos son un poco más complejos, así que dedicaremos algo de tiempo a analizarlos.
Valores compuestos
Los arreglos son listas ordenadas de uno o más valores encerrados entre corchetes. Puedes tener un arreglo de cadenas de texto, uno de números, uno de arreglos, uno de tipos mixtos, etc.:
["alice", "bob", "carlotta"][2, -5, 3.8][[1, 2, 3], [4, 5, 6]][true, "banana", 17]
Puedes acceder a cualquier elemento de un arreglo mediante su índice o posición en el arreglo. Los índices siempre empiezan en 0, por lo que el primer elemento tiene el índice 0, el segundo el índice 1, y así sucesivamente, como se muestra en el arreglo users:
Si quieres obtener el primer elemento de la lista users, usarías esta sintaxis:
Si quieres asignar el segundo elemento a la variable admin, lo indicarías así:
Si usas la sintaxis users[3] para intentar obtener un cuarto elemento (inexistente) de la lista, OPA no encontrará coincidencias y el resultado será un conjunto vacío {} (indefinido).
Los objetos son listas desordenadas de uno o más pares de clave-valor encerrados entre llaves. La clave y el valor pueden ser de cualquier tipo y no tienen que coincidir. Cada clave y su valor correspondiente se unen con dos puntos:
{ "alice": "admin", "bob": "user", "carlotta": "user" }{ "ports": [80, 443] }{ 80: true }
Puedes acceder al valor de un objeto especificando su clave. Por ejemplo, el objeto users de abajo contiene tres pares de clave-valor:
En una consulta, para obtener el valor del par cuya clave es "alice", usarías esta sintaxis:
Los conjuntos son listas desordenadas de uno o más valores únicos, también encerrados entre llaves. Los valores pueden ser de cualquier tipo:
{1, 2, 3}{"alice", "bob", "carlotta"}
Como los conjuntos no tienen orden, dos conjuntos pueden ser iguales si contienen los mismos elementos, aunque estén en distinto orden. Por ejemplo, {1, 2, 3} es igual a {2, 3, 1}.
Puedes comprobar si un elemento está en un conjunto. Supongamos que tienes el siguiente conjunto:
En una consulta, para comprobar si el entero 4 está en el conjunto nums, usarías esta sintaxis:
Tipos de reglas
En Rego hay varios tipos de reglas:
Reglas completas que producen un solo resultado.
Reglas que generan conjuntos.
Reglas que generan objetos.
Funciones, que en realidad son un poco distintas de las reglas.
Cada una de estas reglas puede tener un cuerpo compuesto por consultas. La diferencia está en cómo se construyen las consultas y qué información devuelven. En la siguiente sección, explicaremos cada tipo de regla, empezando por las reglas completas.
Reglas completas
Si has seguido esta serie, ya viste una regla completa:
Una regla completa asigna un único valor a una variable. En el ejemplo anterior, a la variable allow se le asignaría el valor true si se cumple la condición de la consulta.
Aquí tienes otro ejemplo: esta vez, el número 80 se asigna a la variable allowed_port si se cumple la condición de la consulta:
Una regla completa puede tener una sola consulta, como en el ejemplo anterior, o varias, como vimos en otro ejemplo de la parte 2:
Constantes
¿Qué pasa si quieres que una variable conserve un valor determinado sin importar qué? En otros lenguajes de programación, esto se conoce como constante.
Podrías escribirlo así:
Como la condición de la consulta siempre se cumple, se evalúa como true sin importar qué: la variable pi siempre tiene el valor 3.14.
Gracias a una simplificación sintáctica, puedes omitir por completo el cuerpo:
Lo anterior es una regla completa y, sin importar dónde hagas referencia a pi en el programa, siempre representa 3.14.
Comprensiones de conjuntos y objetos
En muchos casos, quizá quieras asignar a una variable una colección de valores. Para eso sirven las comprensiones de conjuntos y las comprensiones de objetos.
Comprensiones de conjuntos
Una comprensión de conjuntos agrega elementos a una variable, uno a la vez, y produce un conjunto. A veces, puede que quieras recorrer el archivo de entrada para generar un conjunto con todos los valores que cumplan ciertas condiciones. Puedes usar una comprensión de conjuntos para hacerlo.
Supongamos que tienes este documento de entrada, que representa un arreglo con todos los usuarios que tienen una sesión iniciada en el sistema:
¿Recuerdas la política de la empresa que establece que solo Alice tiene permisos de administración? Supongamos que quieres crear una lista única (un conjunto) con todos los usuarios sin permisos de administración que tienen una sesión iniciada.
Podrías generar un conjunto así:
Para explicarlo, primero veremos el cuerpo de la regla y luego la cabeza.
Cuerpo de la regla: Esto indica a OPA que recorra el arreglo input.users en busca de elementos que no coincidan con "alice".
¿Cómo funciona? Antes mencionamos que se hace referencia a un elemento de un arreglo mediante su índice. Aquí usamos la variable i para representar el índice (aunque puedes ponerle otro nombre), porque se incrementa en cada recorrido de la entrada. Al ejecutar las consultas, OPA recorre la lista users y sustituye i por el índice de cada elemento para obtenerlos uno a la vez. Si conoces la programación imperativa, esto se parece a cómo funciona un ciclo imperativo, aunque no es exactamente lo mismo.
La primera vez que OPA recorre el arreglo users de la entrada, esto es lo que ocurre internamente:
En el segundo recorrido, ocurre lo siguiente:
En el tercer recorrido, ocurre lo siguiente:
Cabeza de la regla: La variable nonadmins hace referencia al conjunto en sí, y la variable name representa cada nombre único del conjunto (que en el cuerpo es input.users[i], como explicamos antes). Cuando OPA sigue la lógica del cuerpo, si el valor actual de name cumple todas las condiciones, ese valor se agrega al conjunto nonadmins.
Todo junto: En resumen, en cada recorrido de la lista de usuarios, OPA agrega el valor actual de input.users[i] al conjunto nonadmins[name] si el valor cumple todas las condiciones indicadas en las consultas.
El resultado es este conjunto nonadmins:
¡Y podemos ver que Bob y Carlotta son los usuarios sin permisos de administración que están de turno!
Iteración
Una nota sobre la iteración en Rego: la iteración es implícita. A diferencia de otros lenguajes, como Python, aquí no hay ciclos del tipo «while x == true» o «for y in z». En cambio, recorres un arreglo, conjunto u objeto usando una variable en lugar de un índice de arreglo, un elemento de conjunto o una clave de objeto, como hicimos con i en el arreglo input.users:
Como pusimos una variable entre corchetes, OPA sabe que no hacemos referencia a un único valor específico: los recorremos todos, uno a la vez.
Esto puede requerir un ajuste si estás acostumbrado a los ciclos imperativos, ¡pero es conciso! Lo anterior equivale a la siguiente expresión de Python:
O, si prefieres:
El operador de guion bajo
Si solo necesitas hacer referencia al índice una vez, puedes usar el operador comodín (un guion bajo) en lugar de una variable con nombre, como i:
Cuando se usa como iterador (como la variable i), el operador comodín representa cualquier valor de un arreglo, cualquier elemento de un conjunto o cualquier clave de un objeto. En la práctica, el comodín es una variable sin nombre: una variable descartable. En la regla anterior, OPA comprueba si algún nombre del arreglo input.users es distinto de "alice" (y, si lo es, lo agrega al conjunto nonadmins). El resultado final es exactamente el mismo que si hubieras usado name := input.users[i].
Por otro lado, si necesitas llevar un registro del índice en una regla, te conviene usar una variable con nombre, como se muestra a continuación:
Al usar el siguiente documento de entrada…
… verías el siguiente resultado:
En este caso, necesitamos usar una variable con nombre porque queremos asegurarnos de que el nombre que comprobamos en la consulta input.users[i] != "alice" sea el mismo que mostramos en la consulta msg. Por eso es importante llevar un registro del índice. Es más fácil entenderlo si observas lo que hace OPA internamente, sustituyendo la variable i por un índice. Aquí tienes un ejemplo de una iteración por el arreglo input.users:
Debemos usar input.users[2] en ambas consultas para asegurarnos de hacer referencia al valor del mismo índice (en este caso, "carlotta").
Comprensión de objetos
Una comprensión de objetos agrega elementos a una variable, uno a la vez, y produce un objeto, de forma similar a como las reglas de conjuntos generan conjuntos. Pero mientras que el propósito de generar un conjunto es crear una colección de valores únicos, el objetivo de una regla de objetos es producir una colección de pares de clave-valor.
El proceso es muy parecido a escribir una regla de conjuntos. Sin embargo, en una regla de objetos, especificas la parte del valor del par de clave-valor en la cabeza de la regla:
Este ejemplo es exactamente igual al de nuestra primera regla de conjuntos, salvo que aquí se declara que el valor de cada par de clave-valor es "logged-in".
Usemos el mismo documento de entrada:
Al evaluar la regla con la entrada anterior, el resultado es:
El resultado es un objeto nonadmins que contiene dos pares de clave-valor. Cada par tiene un nombre de usuario como clave y "logged-in" como valor.
Funciones
Las funciones de Rego son similares a las de otros lenguajes: ofrecen una manera modular y reutilizable de indicarle al programa que haga algo. La sintaxis de las funciones es similar a la de las reglas, y ambas declaran consultas de la misma manera, pero una función incluye un parámetro (que sirve como marcador de posición para un argumento real) entre paréntesis.
Por ejemplo, la función siguiente toma el valor de x, lo duplica y asigna el valor resultante a y.
En otra parte del paquete, puedes llamar a la función proporcionándole un argumento con el que operar, así:
Y z tendría el valor 4.
Puedes volver a llamarla pasando el argumento 12 y asignando el resultado a foo; foo tendría el valor 24:
Puedes usar una función cuando necesites realizar una tarea muy específica varias veces, especialmente dentro de otras funciones. Esto resulta útil cuando quieres que el código sea más limpio y modular. Si vas a repetir una tarea con diferentes entradas, puedes escribir una función para realizarla.
También puedes escribir funciones «helper» que se usan en otras funciones. A continuación, se asigna el valor true a la variable allow si todos los elementos de la matriz input.tags son válidos. Para determinar si un elemento es válido, las funciones helper is_lowercase_value e is_long_enough comprueban, respectivamente, si un argumento de cadena está en minúsculas o tiene la longitud adecuada. Ambas se usan dentro de la función is_valid, a la que se llama desde allow:
Evaluar reglas de ejemplo con OPA
Experimentemos con reglas de conjuntos, reglas de objetos y funciones en Rego. Como en publicaciones anteriores del blog, nos centraremos en dos formas de interactuar con OPA:
Usar OPA Playground
Usar la herramienta de línea de comandos de OPA
Para ver las instrucciones sobre cómo usar estas interfaces, consulta la Parte 1.
Una vez más, usaremos un ejemplo más cercano al mundo real que incluye un pod de Kubernetes. Este es el manifiesto JSON que usaremos como entrada:
Y esta es la política con la que lo evaluaremos, que hace cumplir el requisito corporativo «Los contenedores de los pods en la etapa de producción no deben usar la imagen más reciente»:
Este conjunto de reglas demuestra varios conceptos que tratamos en esta publicación del blog, como funciones, reglas de conjuntos, iteración y el operador de guion bajo. Así es como funciona:
is_labeled_prod(labels): esta es una función que devuelve true si el conjunto de etiquetas que se le pasa incluye una etiqueta "stage" con el valor "prod" o "production".
latest_containers[container]: esta es una regla de conjunto. OPA usa el operador de guion bajo como iterador para comprobar si alguno de los contenedores de la entrada usa la imagen más reciente y, de ser así, lo agrega al conjunto latest_containers. Determinamos si una imagen es la más reciente comprobando si su nombre termina en ":latest".
deny[msg]: ¡esta también es una regla de conjunto! Es una versión más avanzada de la regla de conjunto deny[msg] que vimos en la Parte 2. Tiene tres consultas, para las que OPA realiza las siguientes acciones:
Comprueba si
input.kindes "Pod".Llama a la función
is_labeled_prody le pasainput.metadata.labelspara comprobar si hay etiquetas"stage"con el valor"prod"o"production".Itera por el conjunto
latest_containerspara comprobar si hay algún contenedor en él.
SI las tres consultas anteriores devuelven true (es decir, si OPA encuentra una coincidencia en la entrada para cada condición), ENTONCES OPA agrega un mensaje personalizado al conjunto deny con el nombre del contenedor que no cumple la política.
Para que te resulte más fácil, creamos un playground que ya incluye este contenido: https://play.openpolicyagent.org/p/HgHE4w2b4y
Si evalúas las reglas seleccionando el botón Evaluate en el playground o ejecutando un comando como opa eval -i input.json -d check_prod_pod.rego "data.rules.check_prod_pod" --format pretty si ejecutas OPA localmente, verás este resultado:
El primer elemento del resultado es el conjunto deny, que muestra un mensaje indicando que el contenedor myapp1 no cumple nuestra política. También puedes ver los elementos del conjunto latest_containers, que incluye el nombre y la imagen de cada contenedor; en este caso, el único contenedor es myapp1.
Veamos qué pasa si cambiamos el nombre de la imagen de myapp2 a myapp2:latest (línea 19). Si volvemos a evaluar las reglas, el conjunto deny ahora incluye un mensaje para myapp1 y myapp2, y el conjunto latest_containers incluye ambos contenedores:
Por último, eliminemos la línea "stage": "prod" (línea 7) para que la entrada quede así:
Si evaluamos las reglas ahora, vemos que latest_containers todavía incluye ambos contenedores, pero el conjunto deny está vacío:
Como el pod no está etiquetado para producción, es válido (según la política corporativa) que sus contenedores usen la imagen más reciente. ¡Por lo tanto, el pod cumple con la política!
¿Qué sigue?
¡Felicitaciones! Terminaste nuestra serie de tres publicaciones del blog sobre cómo escribir en Rego.
Si quieres obtener más información, aquí tienes algunos recursos útiles:
Si te interesa usar Rego para escribir reglas personalizadas para Snyk IaC, consulta nuestra documentación aquí. Además de los conjuntos de reglas integrados de Snyk para seguridad y cumplimiento, las reglas personalizadas de IaC+ te permiten establecer controles de seguridad a medida en todo tu SDLC.
IaC+ te ofrece una vista única y controles para los problemas de configuración, desde el código hasta la nube, con una interfaz de problemas, un conjunto de reglas y un motor de políticas que abarcan IDE, SCM, CLI, CI/CD, Terraform Cloud y entornos de nube implementados, como AWS, Azure y Google Cloud.
Empieza con Capture the Flag
Aprende a resolver desafíos de Capture the Flag viendo nuestro taller virtual introductorio a pedido.



