Skip to main content

5 consejos para usar el lenguaje Rego con Open Policy Agent (OPA)

header cloud security

18 de septiembre de 2020

0 minutos de lectura

En Fugue, nos encanta Open Policy Agent (OPA), y hemos escrito mucho código Rego para proteger los recursos en la nube. Por eso, reunimos las lecciones más valiosas que aprendimos en el proceso. También puedes usar OPA y el lenguaje Rego para habilitar políticas como código y aplicar automáticamente políticas codificadas.

1. Las leyes de De Morgan serán tus mejores aliadas

Si tuviera que limitar esta lista a un solo punto, sería este. Las leyes de De Morgan son dos reglas de transformación. Aplicadas a Rego, te permiten transformar una regla y mantener exactamente el mismo comportamiento.

Suelen escribirse así:

¬(A ∧ B) ⇔ ¬A ∨ ¬B
¬(A ∨ B) ⇔ ¬A ∧ ¬B

O, en código:

!(a && b) <=> !a || !b
!(a || b) <=> !a && !b

Pero quizá un ejemplo lo aclare mejor. Las dos afirmaciones siguientes son equivalentes y ejemplifican la aplicación de la primera ley:

  • Esta no es una pizza con jamón y champiñones.

  • Esta pizza no tiene jamón o no tiene champiñones.

Te permiten transformar cláusulas AND en cláusulas OR y viceversa, sin cambiar el comportamiento del código. Un momento —podrías preguntar—, si estas transformaciones no cambian el comportamiento de mi código, ¿para qué sirven?

La idea clave es que Rego, como lenguaje de consulta, se basa principalmente en disyunciones (o afirmaciones OR). Por ejemplo, comprobar si alguien del grupo está capacitado para cortar una pizza se puede escribir así:

default allow = false

allow {
  input.people[_].profession == "mathematician"
}

Pero ¿cómo comprobamos si todas las personas del grupo están capacitadas para cortar pizzas? ¿Podemos, mmm, crear primero un grupo con las personas capacitadas y luego comprobar su tamaño?

qualified_pizza_cutters[name] = person {
  person = input.people[name]
  person.profession == "mathematician"
}

default allow = false

allow {
  count(qualified_pizza_cutters) == count(input.people)
}

Esto no es muy bueno. Además de ser difícil de leer, una afirmación como esta es difícil de optimizar para un planificador de consultas, ya que tenemos que contar estos conjuntos; incluso si podríamos detenernos en cuanto encontramos a una persona menor de edad en el grupo.

Aquí es donde entran en juego las leyes de De Morgan: nos indican que siempre escribimos las consultas como una disyunción al negar algunas partes. Solo tenemos que agregar un enunciado auxiliar para negar el enunciado original:

cannot_cut_pizza {
  input.people[_].profession != "mathematician"
}

Y luego escribir la política es sencillo:

default allow = false

allow {
  not cannot_cut_pizza
}

¡Listo! Es eficiente y fácil de leer.

Conclusión: si parece difícil escribir una política Rego, considera siempre si es más fácil escribir la política negada y luego volver a negarla. ¡Viva la lógica!

2. ¿or o any?

Como acabamos de ver, Rego es más fácil de leer y escribir si colocas las afirmaciones OR al principio y las afirmaciones AND, una tras otra, dentro de los cuerpos de las reglas. Sin embargo, como programadores, no suele pasar mucho tiempo antes de encontrar ese caso en el que lo idiomático no funciona bien.

En Rego, esto suele ocurrir cuando el cuerpo de una regla ya tiene una buena cantidad de consultas. Por ejemplo:

probably_a_pizza {
  input.shape == 'circle'
  input.ingredients[_] == 'cheese'
  input.ingredients[_] == 'tomato_sauce'
}

¿Y si también queremos permitir porciones de pizza? Podemos duplicar todo el cuerpo:

probably_a_pizza {
  input.shape == 'circle'
  input.ingredients[_] == 'cheese'
  input.ingredients[_] == 'tomato_sauce'
} {
  input.shape == 'circular_sector'
  input.ingredients[_] == 'cheese'
  input.ingredients[_] == 'tomato_sauce'
}

Bueno, eso no es muy bueno. En la mayoría de los casos podemos introducir una regla auxiliar:

has_a_pizza_shape {
  input.shape == 'circle'
} {
  input.shape == 'circular_sector'
}

probably_a_pizza {
  has_a_pizza_shape
  input.ingredients[_] == 'cheese'
  input.ingredients[_] == 'tomato_sauce'
}

En este caso funciona bien. Sin embargo, a veces tenemos tantas reglas auxiliares que hasta cuesta encontrarles un buen nombre. En esos casos poco frecuentes, podemos fingir que en Rego existe una afirmación OR llamada any:

probably_a_pizza {
  any([input.shape == 'circle', input.shape == 'circular_sector'])
  input.ingredients[_] == 'cheese'
  input.ingredients[_] == 'tomato_sauce'
}

Esta es una forma perfectamente legible de escribirlo. Si usas any con moderación, puede facilitarte mucho la vida.

3. Comprensiones any y all

Siguiendo por el mismo camino, any y all son extremadamente útiles en más casos, especialmente con las comprensiones de listas. Estas dos funciones juntas son mucho más que la suma de sus partes.

Si volvemos al primer ejemplo, así podemos usar all junto con una comprensión de lista para expresar la misma política:

allow {
  all([person.profession == "mathematician" | person = input.people[_]])
}

Creo que esta es tan fácil de leer como la versión que construimos siguiendo las leyes de De Morgan. Cuál es más idiomática suele depender del contexto.

  1. ¿Es más fácil razonar sobre la política negada? Entonces seguiremos la transformación de De Morgan.

  2. ¿O es más fácil pensar en los elementos que vamos a controlar como una lista o colección? En ese caso, una comprensión de lista es adecuada.

4. Usa walk para recorrer árboles de documentos completos

Las consultas Rego siempre terminan, lo cual es excelente, pero tiene un costo: por lo general, no es posible escribir reglas recursivas.

¿Por qué son necesarias las reglas recursivas? Son muy útiles para trabajar con documentos de entrada recursivos, como árboles profundamente anidados. Por suerte, OPA proporciona algunas funciones integradas para ayudarte.

Por ejemplo, podríamos estar escribiendo una política para monitorear hilos de discusión:

[
  {
    "author": "Alice",
    "message": "I think New York is at least as good as Italy for pizza",
    "replies": [
      {
        "author": "Bob",
        "message": "It's because of the water!",
        "replies": [
          {
            "Author": "Alice",
            "message": "I've heard that before but I'm not convinced.",
            "replies": []
          }
        ]
      },
    ]
  },
  {
    "author": "Charlie",
    "message": "You are a horrible person.",
    "replies": []
  }
]

¿Cómo obtenemos los mensajes en Rego? Podemos probar algo como esto:

messages[message] {
  message = input[_].message
} {
  message = input[_].replies[_].message
}
  message = input[_].replies[_].replies[_].message
}

Esto funciona bien con niveles de anidamiento finitos, pero no siempre podemos dar eso por sentado y, además, no es muy fácil de leer. En su lugar, puedes usar walk para recorrer recursivamente el árbol y obtener todos los campos message:

messages[message] {
  [_, value] := walk(walk_input)
  message = value.message
}

Una vez que tenemos esos mensajes en una regla Rego común, podemos escribir una política idiomática para denegar los hilos de discusión que contengan malas palabras.

deny {
  swear_words := {"hawaii", "pineapple"}
  contains(lower(messages[_]), swear_words[_])
}

Un ejemplo del mundo real en el que esto ocurre son los módulos secundarios de Terraform: aquí usamos walk en Regula para aplanar los módulos secundarios de una forma muy similar a como se aplanan los mensajes de un hilo de discusión.

5. Carga dinámica de paquetes y reglas

Uno de los aspectos interesantes del diseño de Rego es que todo el «universo» de reglas y datos está anidado en el mismo documento. Ya sea que accedas a la entrada del usuario, a datos de archivos JSON o YAML, o a reglas de tus paquetes, todo son referencias:

input.people[0].order
data.recipes.pizza.pepperoni
data.policies.strict.allow

Como podemos enumerar referencias en Rego mediante variables o comodines, podemos inspeccionar dinámicamente el conjunto de paquetes que se cargó. Esto te permite crear una arquitectura en la que solo tienes que agregar nuevos archivos de políticas y resumirlos todos en un informe.

Colocar las políticas en paquetes separados también tiene la ventaja de que se pueden depurar por separado y no hay posibilidad de que los nombres de las reglas entren en conflicto. Las colocaremos todas en el espacio de nombres data.policies. La primera usa las leyes de De Morgan para comprobar si hay queso en una pizza:

package policies.cheese

contains_cheese {
  input.toppings[_] == "cheese"
}

deny["must contain cheese"] {
  not contains_cheese
}

La segunda política verifica que dos personas no empiecen a pelearse por la última porción:

package policies.slices

deny["must be able to share with two people"] {
  input.slices % 2 != 0
}

Una vez que acordamos un espacio de nombres para nuestras políticas, resumirlas es relativamente sencillo: buscamos cada deny en data.policies y generamos un informe legible para las personas.

package summary

report = msg {
  denies := {m | m := data.policies[_].deny[_]}
  msg := sprintf("%d policies failed\n%s", [
    count(denies),
    concat("\n", [sprintf("- %s", [m]) | denies[m]]),
  ])
}

Junto con algo de jq para extraer los valores concretos, esto puede generar un informe bien presentado directamente en tu terminal. Aquí usaremos -I para leer la entrada desde stdin.

$ opa eval -I 'data.summary.report' -d . --format json | \
    jq -r '.result | .[] | .expressions | .[] | .value'
{"toppings": ["crab", "tomato sauce"], "slices": 6}
1 policies failed
- must contain cheese

Hay muchos otros trucos avanzados que se pueden hacer accediendo a los paquetes como datos; por ejemplo, pasar input a un formato diferente según si un paquete declara input_version = 1 o input_version = 2. ¡Todo dentro de Rego!

Leer más

feature customer snowflake
Article

Seguridad desde el inicio: presentamos la integración de Snyk Studio con Snowflake Cortex Code

Snyk Studio se integra con Snowflake Cortex Code para analizar código generado por IA, dependencias y contenedores en busca de vulnerabilidades durante el desarrollo.

Blog

Verano en los estadios: la gira Snyk Connect Fan Zone

La gira Fan Zone de Snyk llevó talleres de seguridad de IA, networking y competencia amistosa a 8 ciudades y 3 sesiones virtuales. Los asistentes desarrollaron habilidades, compartieron ideas y crecieron juntos.

Blog

Snyk VulnBench JS 1.0: ¿Pueden los LLM encontrar los mismos errores dos veces?

Snyk VulnBench JS 1.0: 300 análisis repetidos muestran que los hallazgos de seguridad de los LLM varían en cada ejecución, mientras que SAST y los modelos detectan distintas vulnerabilidades.