Skip to main content

Rego 102: combinando consultas com AND/OR e mensagens personalizadas

feature cloud security

9 de novembro de 2023

0 minutos de leitura

Esta série de posts apresenta uma introdução acessível ao Rego, a linguagem de políticas criada pelos responsáveis pelo mecanismo Open Policy Agent (OPA). Se você está começando e quer aprender a escrever políticas como código com Rego, está no lugar certo.

Nesta série de três partes, vamos abordar:

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 a incubação em 2021.

O Rego é usado para escrever políticas como código, aplicando práticas de programação, como controle de versão e design modular, para avaliar recursos de nuvem e de infraestrutura como código (IaC). O OPA é o mecanismo que avalia políticas como código escritas em Rego. E a Snyk usa a linguagem Rego para regras personalizadas.

Recapitulando a Parte 1

Na Parte 1 desta série de posts, explicamos que uma regra em Rego é uma atribuição condicional. Uma regra consulta a entrada para encontrar uma correspondência com uma condição e, se encontrar, atribui um valor a uma variável.

Uma regra pode ser lida assim:

THIS VARIABLE    :=	HAS THIS VALUE {
    IF THESE CONDITIONS ARE MET
}

Este é o exemplo que usamos, que representa uma política corporativa segundo a qual somente Alice, uma administradora de rede, deve ter permissão para criar e excluir redes virtuais na conta de produção:

allow := true {
  input.user == "alice"
}

O OPA avalia um documento de entrada JSON ou YAML em relação a uma regra para gerar uma decisão de política. O documento de entrada abaixo representa o usuário que está conectado no momento:

{
  "user": "alice"
}

Se você usar o OPA para avaliar a entrada em relação a essa regra, ele encontrará uma correspondência para a consulta input.user == "alice". Portanto, a variável no cabeçalho da regra, allow, recebe o valor definido no cabeçalho: true. Veja a saída que comprova isso:

{
  "allow": true
}

O OPA decidiu que Alice, a usuária conectada no momento, tem permissão para criar e excluir redes virtuais na conta de produção. A entrada está em conformidade com a regra.

AND e OR

Até agora, mostramos apenas regras com uma única consulta. Uma regra também pode conter várias consultas. Nesse caso, elas representam condições que todas precisam ser atendidas para que uma variável receba um valor. Há um AND implícito — "Esta condição precisa ser atendida E esta condição também precisa ser atendida".

Por exemplo, na regra abaixo, tanto input.user == "alice" AND input.environment == "prod" precisam ser verdadeiros para que a variável allow receba o valor true:

allow := true {
  input.user == "alice"
  input.environment == "prod"
}

Em alguns casos, OR pode ser mais adequado. Você pode representar OR usando o mesmo cabeçalho em várias regras:

allow = true {
  input.user == "alice"
}

allow = true {
  input.user == "bob"
}

Este conjunto de regras pode ser lido assim:

allow é true se user for "alice" OU se user for "bob".

Tecnicamente, o conjunto de regras forma uma única regra, porque ambas têm o mesmo cabeçalho. Como você define essa regra em várias etapas, ela é chamada de regra incremental. 

Se quiser, você pode remover o segundo cabeçalho e reunir os corpos. Uma forma mais concisa de escrever o exemplo acima é:

allow = true {
  input.user == "alice"
} {
  input.user == "bob"
}

Talvez você esteja se perguntando por que usamos o operador de unificação = em vez do operador de atribuição :=. Isso acontece porque as variáveis são imutáveis em Rego. Embora as regras com o mesmo cabeçalho sejam tratadas como uma única regra incremental, ao tentar usar o operador de atribuição, você acaba "atribuindo" o mesmo valor à variável várias vezes — algo que Rego não permite. Em vez disso, usamos o operador de unificação no cabeçalho da regra, pois ele unifica várias regras com o mesmo nome.

Se for difícil lembrar qual operador usar no cabeçalho da regra, há uma forma mais simples, graças aos valores padrão e a um pouco de açúcar sintático.

Valores padrão nos cabeçalhos das regras

O valor padrão atribuído a uma variável no cabeçalho de uma regra é true. Por isso, o Rego oferece um atalho sintático: quando uma regra atribui o valor true à variável, você pode omitir := true do cabeçalho.

Isso significa que esta regra AND…

allow := true {
  input.user == "alice"
  input.environment == "prod"
}

…é igual a esta regra AND:

allow {
  input.user == "alice"
  input.environment == "prod"
}

E, da mesma forma, esta regra OR…

allow = true {
  input.user == "alice"
} {
  input.user == "bob"
}

...é igual a esta regra OR:

allow {
  input.user == "alice"
} {
  input.user == "bob"
}

"Ótimo", mesmo!

A palavra-chave default

Como explicamos na Parte 1, se nenhuma entrada corresponder à consulta de uma regra, a variável no cabeçalho da regra não recebe o valor indicado no cabeçalho.

Para demonstrar isso, vamos voltar ao exemplo de regra que diz que somente Alice, uma administradora de rede, deve ter permissão para criar e excluir redes virtuais na conta de produção:

allow := true {
  input.user == "alice"
}

E vamos supor que temos um documento de entrada em que o usuário conectado no momento é Bob:

{
  "user": "bob"
}

Como input.user não é "alice", quando avaliamos a regra em relação à entrada, o OPA não encontra uma correspondência. Portanto, allow não recebe o valor true, e o resultado da avaliação é um conjunto vazio:

{}

Nesse caso, dizemos que o valor de allow é indefinido. Sempre que o OPA consulta a entrada para avaliar uma regra, ele retorna somente valores que correspondem. Se não houver nenhum valor correspondente, não há nada a retornar — por isso, o conjunto vazio.

E se quisermos que o OPA retorne false quando allow não for explicitamente true? Podemos usar a palavra-chave default para definir um valor padrão. Isso significa que, se a avaliação de uma regra não resultar explicitamente em true, ela retorna um valor específico (neste caso, false) em vez de um conjunto vazio de resultados. Para isso, escrevemos uma regra adicional que também usa allow:

default allow = false

Agora, se o OPA determinar que input.user não é "alice", allow não resulta em um conjunto vazio. Em vez disso, recebe o valor padrão que definimos como false:

{
  "allow": false
}

Observe que, ao especificar a palavra-chave default, você usa o operador de unificação = em vez do operador de atribuição := tanto na regra que define o valor padrão quanto na regra que define a atribuição condicional. Mais uma vez, isso acontece porque as variáveis são imutáveis. Se você tentar usar o operador de atribuição, estará "atribuindo" o mesmo valor à variável mais de uma vez, o que Rego não permite. Em vez disso, usamos o operador de unificação:

default allow = false

allow = true {
  input.user == "alice"
}

É claro que você pode aproveitar o açúcar sintático que descrevemos antes e omitir = true na regra com a atribuição condicional. Isso é perfeitamente válido e talvez mais fácil, porque você não precisa se lembrar de qual operador usar na atribuição condicional:

default allow = false

allow {
  input.user == "alice"
}

Mensagens personalizadas

Às vezes, você quer retornar uma série de mensagens em vez de um simples resultado de aprovação ou reprovação: true, false ou indefinido. Para isso, use o cabeçalho de regra deny[msg] e atribua a mensagem desejada à variável msg. A regra abaixo verifica se o usuário não é Alice e, nesse caso, atribui a string "User is denied access" a msg, que é então adicionada ao conjunto deny (falaremos mais sobre conjuntos na Parte 3):

deny[msg] {
  input.user != "alice"
  msg := "User is denied access"
}

Para testar, vamos supor que nosso documento de entrada contenha o nome do usuário conectado no momento:

{
	"user": "bob"
}

Quando avaliamos a regra em relação à entrada acima usando o Rego Playground do OPA ou o comando opa eval -i input.json -d check_user.rego "data.rules.check_user" --format pretty (consulte as instruções no post da Parte 1), o resultado fica assim:

{
  "deny": [
	"User is denied access"
  ]
}

O que está acontecendo? Na verdade, estamos criando uma regra de conjunto, um conceito que retomaremos no próximo post desta série. Por enquanto, basta entender que, em vez de um único resultado true, false ou indefinido, retornamos um conjunto de mensagens atribuídas à variável deny. Em Rego, um conjunto é uma lista não ordenada de elementos únicos, como números inteiros { 1, 2, 3 }, strings { "alice", "bob", "carlotta" } ou até outros conjuntos { { 1, 2}, {3, 4} }. Você pode criar um conjunto com qualquer tipo compatível com Rego ou até combinar diferentes tipos em um mesmo conjunto.

No exemplo de regra, cada elemento do conjunto deny é uma string que contém uma mensagem. Para este documento de entrada específico, há apenas um elemento no conjunto, mas pode haver mais — e mostraremos um exemplo mais adiante neste post.

E se quisermos incluir informações adicionais na mensagem? Podemos usar a função integrada sprintf para exibir o valor do campo input.user que causou o resultado deny:

deny[msg] {
  input.user != "alice"
  msg := sprintf("User %v is denied access", [input.user])
}

A função sprintf recebe dois argumentos: uma string e uma matriz de valores. Neste caso, o único elemento da matriz é uma string representada por input.user. Usamos %v como marcador na primeira string, e o valor da matriz ocupa esse lugar quando a regra é avaliada.

Agora, se avaliarmos a regra usando a seguinte entrada…

{
	"user": "bob"
}

…vemos este resultado:

{
  "deny": [
    "User bob is denied access"
  ]
}

A palavra-chave not

Você pode negar uma expressão colocando a palavra-chave not antes dela, invertendo seu significado. Na maioria das vezes, você vai querer usá-la em uma consulta para especificar a ausência de uma propriedade na entrada. Por exemplo, esta consulta:

input.tags.environment

…significa "O documento de entrada tem a propriedade tags.environment, e o valor não é false", enquanto esta consulta:

not input.tags.environment

…significa "O documento de entrada não tem a propriedade tags.environment ou tags.environment está definida como false." Não há sobreposição nem meio-termo: uma expressão e sua inversa são mutuamente exclusivas.

Veja um exemplo de regra que atribui true a deny se a entrada não tiver uma tag department :

deny {
  not input.tags.department
}

Vamos usar esta entrada:

{
  "tags": {
    "environment": "staging"
  }
}

Se avaliarmos essa entrada em relação à regra acima, veremos que deny retorna true, porque falta a propriedade obrigatória department:

{
  "deny": true
}

Como avaliar uma regra de exemplo com OPA

Vamos colocar em prática os conceitos abordados neste post avaliando uma regra de exemplo. Assim como na Parte 1, vamos nos concentrar em duas formas de interagir com o OPA:

  • Usar o Rego Playground

  • Usar a ferramenta de linha de comando do OPA

Para ver instruções sobre como usar essas interfaces, consulte a Parte 1.

Desta vez, vamos usar um exemplo mais próximo do mundo real, com um pod do Kubernetes. Veja o manifesto JSON que usaremos como entrada:

{
  "apiVersion": "v1",
  "kind": "Pod",
  "metadata": {
    "name": "nginx-demo",
    "labels": {
      "release" : "stable"
    }
  },
  "spec": {
    "containers": [
      {
        "name": "nginx",
        "image": "nginx:1.14.2",
        "ports": [
          {
            "containerPort": 80
          }
        ]
      }
    ]
  }
}

E esta é a regra que vamos avaliar em relação a ele, criada para aplicar a política da empresa: "Os pods do Kubernetes devem ter os rótulos release e environment":

deny[msg] {
  input.kind == "Pod"
  not input.metadata.labels.release
  msg := sprintf("Pod %v is missing release label", [input.metadata.name])
} {
  input.kind == "Pod"
  not input.metadata.labels.environment
  msg := sprintf("Pod %v is missing environment label", [input.metadata.name])
}

Esta regra demonstra alguns dos conceitos abordados neste post:

  • deny[msg] para retornar um conjunto de mensagens personalizadas em vez de true ou false/undefined

  • Estrutura de regras com AND e OR:

    • Negar se o objeto do Kubernetes for um pod AND não tiver o rótulo release OR:

    • Negar se o objeto do Kubernetes for um pod AND não tiver o rótulo environment

  • A palavra-chave not para verificar se uma propriedade está ausente

  • A função sprintf para retornar uma mensagem com o nome do pod em não conformidade

Para facilitar, criamos um playground que já contém este conteúdo: https://play.openpolicyagent.org/p/KNVK9kEvIT 

Se você avaliar a regra clicando no botão Evaluate no playground ou executando um comando como opa eval -i input.json -d check_pod.rego "data.rules.check_pod" --format pretty ao executar o OPA localmente, verá esta saída:

{
  "deny": [
    "Pod nginx-demo is missing environment label"
  ]
}

Como podemos ver, o pod do Kubernetes que estamos verificando não está em conformidade com nossa regra porque a entrada não contém uma propriedade labels.environment .

Agora, vamos remover a propriedade labels.release. A seção labels da entrada deve ficar assim:

    "labels": {
    }

Se você avaliar a regra agora, verá que o conjunto deny contém duas mensagens:

{
  "deny": [
    "Pod nginx-demo is missing environment label",
    "Pod nginx-demo is missing release label"
  ]
}

Por fim, vamos adicionar as propriedades labels.release e labels.environment à entrada, para que ela fique assim:

    "labels": {
      "release" : "stable",
      "environment": "prod"
    }

O que acontece se avaliarmos a regra novamente? Vemos que o conjunto deny está vazio:

{
  "deny": []
}

Isso significa que nosso pod está em conformidade, pois o OPA não adicionou nenhuma mensagem ao conjunto deny. Viva!

E agora?

Volte ao nosso blog para ler a Parte 3 de Rego para iniciantes, na qual vamos explorar regras de conjunto, regras de objeto, funções e iteração.

Enquanto isso, confira estes recursos úteis:

Se você tem interesse em usar Rego para criar regras personalizadas para o Snyk IaC, confira nossa documentação. Além dos conjuntos de regras integrados de segurança e conformidade da Snyk, 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 os 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.

Leia mais

feature customer snowflake
Article

Segurança desde o início: conheça a integração do Snyk Studio com o Snowflake Cortex Code

O Snyk Studio se integra ao Snowflake Cortex Code para detectar vulnerabilidades em código gerado por IA, dependências e contêineres durante o desenvolvimento.

blog feature pypi spoof
Article

A Snyk AI Security Platform completa, grátis para mantenedores de projetos open source

Mantenedores de projetos open source estão sobrecarregados com relatórios reais de vulnerabilidades e precisam de ajuda para priorizar, corrigir e lançar correções mais rápido. O Secure Developer Program da Snyk dá aos projetos qualificados acesso gratuito à Snyk AI Security Platform.

blog feature snyk code green
Blog

Segurança no nível de tipos: o futuro da geração segura de código com IA?

Tipos seguros desde a concepção podem transformar bugs comuns em erros de compilação. Neste artigo, veja como a segurança no nível de tipos pode ajudar a prevenir classes inteiras de vulnerabilidades geradas por IA.