Rego 103: tipos de valores e regras
16 de novembro de 2023
0 minutos de leituraEsta série de posts oferece uma introdução simples ao Rego, a linguagem de políticas criada pelos desenvolvedores do mecanismo Open Policy Agent (OPA). Se você está começando e quer aprender a escrever políticas como código em Rego, está no lugar certo.
Nesta série de três partes, vamos abordar:
Parte 1: Rego 101: Introdução ao Rego
Parte 2: Rego 102: combinação de consultas com AND/OR e mensagens personalizadas
Parte 3 (esta parte!): tipos de valores e regras
Para relembrar, Rego é uma linguagem declarativa de consultas, criada pelos desenvolvedores do framework Open Policy Agent (OPA). A Cloud Native Computing Foundation (CNCF) aceitou o OPA como projeto hospedado em nível de incubação em abril de 2019, e o OPA concluiu o período de incubação em 2021.
Rego é usado para escrever políticas como código, aplicando práticas de programação, como controle de versão e design modular, à avaliação de recursos de nuvem e infraestrutura como código (IaC). O OPA é o mecanismo que avalia as políticas como código escritas em Rego. A Snyk usa a linguagem Rego para criar regras personalizadas.
Revisão da Parte 2
Na Parte 2, mostramos como usar:
Regras AND e OR
A palavra-chave
defaultA palavra-chave
notMensagens
denypersonalizadas
Nesta parte, vamos concluir a série com foco em regras de conjuntos, regras de objetos, funções e iteração.
Tipos de valores
Em Rego, um valor é uma representação de algum tipo de dado. Cada valor pertence a um tipo específico. Os tipos de Rego se dividem em duas categorias: escalares e compostos.
Valores escalares representam uma única unidade de dados e incluem os seguintes tipos:
Strings são delimitadas por aspas duplas.
Os números incluem inteiros positivos e negativos e números decimais.
Valores booleanos podem ser apenas
trueoufalse.nullrepresenta a ausência de um valor.
Valores compostos representam uma coleção de valores e incluem os seguintes tipos:
Arrays
Objetos
Conjuntos
Se você acompanhou esta série de posts, já viu vários exemplos de valores escalares e compostos. Valores compostos são um pouco mais complexos, então vamos nos aprofundar neles.
Valores compostos
Arrays são listas ordenadas de um ou mais valores, delimitadas por colchetes. Você pode ter um array de strings, um array de números, um array de arrays, um array com tipos mistos e assim por diante:
["alice", "bob", "carlotta"][2, -5, 3.8][[1, 2, 3], [4, 5, 6]][true, "banana", 17]
Você pode acessar qualquer elemento de um array usando o índice ou a posição dele no array. Os índices sempre começam em 0: o primeiro item tem índice 0, o segundo tem índice 1, e assim por diante, como mostra o array users abaixo:
Para recuperar o primeiro item da lista users, use esta sintaxe:
Para atribuir o segundo item à variável admin, faça assim:
Se você usar a sintaxe users[3] para tentar recuperar um quarto item (inexistente) da lista, o OPA não encontrará nenhuma correspondência, e a saída será um conjunto vazio {} (indefinido).
Objetos são listas não ordenadas de um ou mais pares chave-valor, delimitadas por chaves. A chave e o valor podem ser de qualquer tipo, e não precisam ser do mesmo tipo. Cada chave e seu valor correspondente são separados por dois-pontos:
{ "alice": "admin", "bob": "user", "carlotta": "user" }{ "ports": [80, 443] }{ 80: true }
Você pode acessar o valor de um objeto especificando sua chave. Por exemplo, o objeto users abaixo contém três pares chave-valor:
Para recuperar em uma consulta o valor do par cuja chave é "alice", use esta sintaxe:
Conjuntos são listas não ordenadas de um ou mais valores únicos, também delimitadas por chaves. Os valores podem ser de qualquer tipo:
{1, 2, 3}{"alice", "bob", "carlotta"}
Como conjuntos não são ordenados, dois conjuntos podem ser iguais se tiverem os mesmos elementos, mesmo que estejam em ordens diferentes. Por exemplo, {1, 2, 3} é igual a {2, 3, 1}.
Você pode verificar se um elemento está em um conjunto. Digamos que você tenha o seguinte conjunto:
Para verificar em uma consulta se o inteiro 4 está no conjunto nums, use esta sintaxe:
Tipos de regras
Há vários tipos de regras em Rego:
Regras completas, que produzem um único resultado.
Regras que geram conjuntos.
Regras que geram objetos.
Funções, que são um pouco diferentes das regras.
Cada uma delas pode ter um corpo composto por consultas. A diferença está na forma como as consultas são construídas e nas informações que retornam. Na próxima seção, vamos explicar cada tipo de regra, começando pelas regras completas.
Regras completas
Se você acompanhou esta série, já viu uma regra completa:
Uma regra completa atribui um único valor a uma variável. No exemplo acima, a variável allow receberia o valor true se a condição da consulta fosse atendida.
Veja outro exemplo, desta vez atribuindo o número 80 à variável allowed_port se a condição da consulta for atendida:
Uma regra completa pode ter uma consulta, como acima, ou várias consultas, como vimos em outro exemplo da Parte 2:
Constantes
E se você quiser que uma variável mantenha um determinado valor independentemente do que aconteça? Em outras linguagens de programação, isso é chamado de constante.
Você poderia escrever assim:
Como a condição da consulta é sempre atendida, ela é avaliada como true em qualquer situação — a variável pi sempre tem o valor 3.14.
Graças a uma simplificação sintática, você pode até omitir o corpo:
O exemplo acima é uma regra completa e, não importa onde você se refira a pi no programa, ela sempre representa 3.14.
Compreensões de conjuntos e objetos
Em muitos casos, você pode querer atribuir à variável uma coleção de valores. É aí que entram as compreensões de conjuntos e as compreensões de objetos.
Compreensões de conjuntos
Uma compreensão de conjunto adiciona elementos a uma variável, um de cada vez, produzindo um conjunto. Às vezes, você pode querer percorrer o arquivo de entrada para gerar um conjunto com todos os valores que atendam a determinadas condições. Para isso, você pode escrever uma compreensão de conjunto.
Imagine que você tenha este documento de entrada, que representa um array de todos os usuários atualmente conectados ao sistema:
Lembra da política da empresa que diz que somente Alice tem permissões administrativas? Digamos que você queira criar uma lista única (um conjunto) de todos os usuários sem privilégios administrativos que estão conectados.
Você poderia gerar um conjunto assim:
Para explicar isso, vamos analisar primeiro o corpo da regra e depois o cabeçalho.
Corpo da regra: isso instrui o OPA a percorrer o array input.users em busca de elementos diferentes de "alice".
Como isso funciona? Como mencionamos antes, você acessa um item de um array pelo índice. Aqui, usamos a variável i para representar o índice (mas você poderia dar outro nome a ela), pois seu valor é incrementado a cada passagem pela entrada. Ao executar as consultas, o OPA percorre a lista users, substituindo i pelo índice de cada item para obter os elementos, um de cada vez. Se você conhece programação imperativa, é parecido com o funcionamento de um loop imperativo, embora não seja exatamente igual.
Na primeira vez que o OPA percorre o array users na entrada, é isto que acontece nos bastidores:
Na segunda passagem, acontece o seguinte:
Na terceira passagem, acontece o seguinte:
Cabeçalho da regra: A variável nonadmins se refere ao próprio conjunto, e a variável name representa cada nome único do conjunto (no corpo, como explicado acima, esse nome é input.users[i]). Quando o OPA segue a lógica do corpo, se o valor atual de name atender a todas as condições, esse valor será adicionado ao conjunto nonadmins.
Juntando tudo: Em resumo, a cada passagem pela lista de usuários, o OPA adiciona o valor atual de input.users[i] ao conjunto nonadmins[name] se ele atender a todas as condições das consultas.
O resultado é este conjunto nonadmins:
Assim, vemos que os usuários sem privilégios administrativos de plantão são Bob e Carlotta!
Iteração
Uma observação sobre a iteração em Rego: a iteração é implícita. Não há loops como “while x == true” ou “for y in z”, ao contrário do que acontece em linguagens como Python. Em vez disso, você percorre um array, conjunto ou objeto usando uma variável no lugar de um índice de array, elemento de conjunto ou chave de objeto, como fizemos com i abaixo, no array input.users:
Como colocamos uma variável entre colchetes, o OPA entende que não estamos nos referindo a um único valor específico: estamos considerando todos eles, um de cada vez.
Isso exige um pequeno ajuste se você está acostumado a loops imperativos, mas é conciso! O exemplo acima equivale à seguinte expressão em Python:
Ou, se preferir:
O operador sublinhado
Se você só precisa se referir ao índice uma vez, pode usar o operador curinga (um sublinhado) em vez de uma variável nomeada, como i:
Quando usado como iterador (como a variável i), o operador curinga representa qualquer valor em um array, qualquer elemento em um conjunto ou qualquer chave em um objeto. Na prática, o curinga é uma variável sem nome — uma variável descartável. Na regra acima, o OPA verifica se algum nome no array input.users é diferente de "alice" (e, se for, adiciona esse nome ao conjunto nonadmins). O resultado final é exatamente o mesmo que se você tivesse usado name := input.users[i].
Por outro lado, se você precisa acompanhar o índice em uma regra, deve usar uma variável nomeada, como abaixo:
Com o seguinte documento de entrada…
… você verá a seguinte saída:
Neste caso, precisamos usar uma variável nomeada porque queremos garantir que o nome verificado na consulta input.users[i] != "alice" seja o mesmo nome exibido na consulta msg. Por isso, é importante acompanhar o índice. Fica mais fácil entender observando o que o OPA faz nos bastidores, substituindo a variável i por um índice. Veja um exemplo de uma iteração pelo array input.users:
Precisamos usar input.users[2] nas duas consultas para garantir que estamos nos referindo ao valor no mesmo índice (neste caso, "carlotta").
Compreensão de objetos
Uma compreensão de objeto adiciona elementos a uma variável, um de cada vez, produzindo um objeto, de forma semelhante a como regras de conjuntos geram conjuntos. Mas, enquanto o objetivo de gerar um conjunto é criar uma coleção de valores únicos, uma regra de objeto busca produzir uma coleção de pares chave-valor.
O processo é muito parecido com escrever uma regra de conjunto. No entanto, em uma regra de objeto, você especifica a parte do valor do par chave-valor no cabeçalho da regra:
Este exemplo é exatamente igual ao nosso primeiro exemplo de regra de conjunto, mas aqui o valor de cada par chave-valor é definido como "logged-in".
Vamos usar o mesmo documento de entrada:
Ao avaliar a regra com a entrada acima, esta é a saída:
O resultado é um objeto nonadmins que contém dois pares chave-valor. Cada par tem um nome de usuário como chave e "logged-in" como valor.
Funções
As funções em Rego são como as funções de outras linguagens: elas oferecem uma maneira modular e reutilizável de instruir o programa a fazer algo. A sintaxe de funções é parecida com a de regras, e ambas declaram consultas da mesma maneira, mas uma função inclui um parâmetro (que serve como espaço reservado para um argumento real) entre parênteses.
Por exemplo, a função abaixo recebe o valor de x, dobra esse valor e atribui o resultado a y.
Em outra parte do pacote, você pode chamá-la fornecendo um argumento para ela processar, assim:
E z seria avaliado como 4.
Você poderia chamá-la novamente, passando o argumento 12 e atribuindo o resultado a foo. Nesse caso, foo seria avaliado como 24:
Você pode usar uma função quando precisar executar uma tarefa muito específica várias vezes, especialmente dentro de outras funções. Isso é útil para deixar o código mais limpo e modular. Se você precisa repetir uma tarefa com entradas diferentes, pode escrever uma função para executá-la.
Você também pode escrever funções auxiliares para usar em outras funções. A variável allow abaixo recebe o valor true se todos os elementos do array input.tags forem válidos. Para determinar se um elemento é válido, as funções auxiliares is_lowercase_value e is_long_enough verificam, respectivamente, se o argumento de string está em letras minúsculas ou tem o comprimento adequado. Ambas são usadas dentro da função is_valid, que é chamada por allow:
Avaliação de regras de exemplo com OPA
Vamos experimentar regras de conjuntos, regras de objetos e funções em Rego. Como nos artigos anteriores, vamos nos concentrar em duas maneiras de interagir com o OPA:
Usar o OPA Playground
Usar a ferramenta de linha de comando do OPA
Para ver instruções sobre como usar essas interfaces, consulte a Parte 1.
Mais uma vez, vamos usar um exemplo mais próximo do mundo real, envolvendo um pod do Kubernetes. Veja o manifesto JSON que usaremos como entrada:
E aqui está a política que usaremos para avaliá-lo, aplicando o requisito corporativo: "Os contêineres em pods de produção não devem usar a imagem latest":
Este conjunto de regras demonstra vários conceitos que abordamos neste artigo, como funções, regras de conjuntos, iteração e o operador sublinhado. Veja como tudo funciona:
is_labeled_prod(labels) — Esta é uma função que retorna true se o conjunto de rótulos informado incluir um rótulo "stage" com o valor "prod" ou "production".
latest_containers[container] — Esta é uma regra de conjunto. O OPA usa o operador sublinhado como iterador para verificar se algum dos contêineres da entrada usa a imagem latest e, se usar, adicioná-lo ao conjunto latest_containers. Para determinar se uma imagem é a latest, verificamos se o nome da imagem termina em ":latest".
deny[msg] — Esta também é uma regra de conjunto! É uma versão mais avançada da regra de conjunto deny[msg] que mostramos na Parte 2. Ela tem três consultas, e o OPA executa as seguintes ações:
Verifica se
input.kindé "Pod".Chama a função
is_labeled_prod, passandoinput.metadata.labelspara verificar se há algum rótulo"stage"com o valor"prod"ou"production".Percorre o conjunto
latest_containerspara verificar se há algum contêiner nele.
SE as três consultas acima forem avaliadas como true (ou seja, se o OPA encontrar uma correspondência na entrada para cada condição), ENTÃO o OPA adicionará uma mensagem personalizada ao conjunto deny, indicando o nome do contêiner que não está em conformidade.
Para facilitar, criamos um playground com esse conteúdo pronto para uso: https://play.openpolicyagent.org/p/HgHE4w2b4y
Se você avaliar as regras clicando no botão Evaluate no playground ou executando um comando como opa eval -i input.json -d check_prod_pod.rego "data.rules.check_prod_pod" --format pretty ao executar o OPA localmente, verá esta saída:
O primeiro item da saída é o conjunto deny, que mostra uma mensagem informando que o contêiner myapp1 não está em conformidade com nossa política. Você também pode ver os elementos do conjunto latest_containers, que inclui o nome e a imagem de cada contêiner — neste caso, o único contêiner é myapp1.
Vamos ver o que acontece se alterarmos o nome da imagem de myapp2 para myapp2:latest (linha 19). Se avaliarmos as regras novamente, o conjunto deny agora incluirá uma mensagem para myapp1 e myapp2, e o conjunto latest_containers incluirá os dois contêineres:
Por fim, vamos remover a linha "stage": "prod" (linha 7) para que a entrada fique assim:
Se avaliarmos as regras agora, veremos que latest_containers ainda inclui os dois contêineres, mas o conjunto deny está vazio:
Como o pod não está rotulado para produção, é aceitável (de acordo com a política corporativa) que seus contêineres usem a imagem latest. Portanto, o pod está em conformidade!
O que vem a seguir?
Parabéns! Você chegou ao fim da nossa série de três artigos sobre como escrever Rego.
Se quiser saber mais, confira estes recursos:
Se você tem interesse em usar Rego para escrever regras personalizadas para o Snyk IaC, confira nossa documentação. Além dos conjuntos de regras integrados da Snyk para segurança e conformidade, as regras personalizadas do IaC+ permitem definir controles de segurança personalizados em todo o seu SDLC.
O IaC+ oferece uma visão unificada e controles para problemas de configuração, do código à nuvem, com uma interface de problemas, um conjunto de regras e um mecanismo de políticas que abrangem IDE, SCM, CLI, CI/CD, Terraform Cloud e ambientes de nuvem implantados, como AWS, Azure e Google Cloud.
Comece a participar de desafios de Capture the Flag
Aprenda a resolver desafios de Capture the Flag assistindo sob demanda ao nosso workshop virtual introdutório.
