In this article
Primeiros passos com Practical Rego
Embora boa parte das informações deste artigo também esteja na referência do Rego oficial, este guia introdutório dá ênfase aos paradigmas de programação (lógico versus procedural), o que é útil para quem programa com linguagens imperativas, como Python ou Java.
Links comuns (por exemplo, Wikipedia) levam a mais informações, enquanto os links entre colchetes (por exemplo, [1]) são fontes das afirmações feitas neste artigo.
Todos os trechos de código deste artigo foram executados com o OPA v0.54.0.
Agradecemos a Jasper Van der Jeugt por ler uma versão preliminar deste artigo e sugerir melhorias.
Introdução ao Rego
O projeto Open Policy Agent (“OPA”) ganhou bastante destaque desde que foi aceito como projeto de incubação da CNCF [0]. O OPA é um mecanismo de políticas de uso geral, usado para aplicar estruturas de autorização. Suas políticas são definidas em uma linguagem de consulta chamada Rego, tema deste artigo.
O Rego é baseado em Datalog, uma linguagem declarativa de consulta lógica. Ele oferece estruturas para correspondência de padrões, filtragem e iteração. O Rego é comparável ao SQL, pois ambos são linguagens de consulta de uso geral. No entanto, o SQL foi projetado para trabalhar com dados tabulares, enquanto o Rego opera com dados no formato JSON. Por ser uma linguagem declarativa de consulta lógica, o Rego tem algumas características pouco comuns em outras linguagens de programação. É essencial compreender essas características para entender o Rego.
Programação declarativa
A programação declarativa expressa a lógica e as regras de um problema sem descrever explicitamente as etapas para resolvê-lo. Ela se concentra no que, e não no como. Em contraste, um paradigma de programação mais popular é o “imperativo”, no qual instruções alteram o estado de um programa para produzir um resultado. Exemplos comuns de linguagens declarativas são SQL ([1], p. 79) e HTML [2]. Linguagens de infraestrutura como código, como HCL, normalmente também adotam a abordagem declarativa, embora muitas vezes combinada a elementos imperativos [3]. Muitas linguagens de programação permitem combinar os dois paradigmas, mas tendem mais à programação imperativa, como Python [4].
Exemplo de programação imperativa (Python):
def is_even(x):
remainder = x % 2
if remainder == 0:
return True
return FalseExemplo de programação lógica declarativa (Rego):
is_even {
remainder == 0
remainder = input % 2
}
No Rego, a ordem das instruções em uma regra não importa.
Programação lógica
O Datalog adota o paradigma de programação lógica, uma forma específica de programação declarativa baseada na lógica formal [5], [6]. Programas lógicos consistem nos chamados fatos e regras, que estabelecem relações e restrições para descrever o domínio do problema. No Rego, esses fatos e regras são expressos usando a cláusula de Horn, uma fórmula lógica também usada no Datalog [6].
Em seguida, um mecanismo de inferência deriva a solução do problema determinando a ordem de execução. Em outras palavras, o mecanismo converte o código em várias fórmulas lógicas e então as resolve. Essa arquitetura ajuda muito a evitar efeitos colaterais, demonstra robustez e torna o sistema mais adequado à verificação formal.
No caso do Rego, o OPA atua como mecanismo de inferência, entre outras funções. Para melhorar as garantias de término do Rego, entre outras medidas, foram feitos esforços para impedir algumas formas de recursão [7], [8]. Além disso, devido às escolhas de projeto mencionadas anteriormente, o Rego também apresenta decidibilidade, outra propriedade computacional crucial [9]. A decidibilidade garante que o processo de tomada de decisão durante a avaliação de políticas sempre produzirá um resultado. Assim, as políticas Rego não ficam travadas e otimizam a complexidade de tempo. Essa robustez provavelmente é um dos principais motivos para usar o Rego em conjunto com o OPA. Ele pode fazer parte de qualquer caminho crítico de uma aplicação de negócios e executar suas funções com confiabilidade.
Como mencionado anteriormente, o Rego consiste em fatos e regras. Cada instrução no código representa uma regra lógica. Se uma regra lógica não puder ser resolvida, não há necessidade de avaliar o restante do código. Consequentemente, cada linha de código precisa ser “truthy”. O Rego herda esse comportamento por ter suas raízes no Datalog; portanto, isso não é algo exclusivo do Rego. Ainda assim, é relevante para quem cria políticas em Rego. Para entender por que é importante estar ciente desse comportamento, leia a seção “Armadilhas”.
Quantificação existencial
Os quantificadores lógicos em programação definem o escopo de uma instrução sobre uma coleção de itens.
O Rego aplica a quantificação existencial (pense em FOR ANY), que consiste em verificar se uma condição é verdadeira para pelo menos um item de uma coleção.
Linguagens de programação imperativas não aplicam por natureza a quantificação da mesma forma que a programação lógica. No entanto, elas oferecem estruturas que permitem verificar se uma condição é verdadeira para todos os itens de uma coleção, o que funciona como a quantificação universal (pense em FOR ALL).
Em ambos os casos, um loop percorre uma coleção e muda seu comportamento ao procurar itens que correspondam às condições (quantificação existencial) ou ao tentar verificar as condições para todos os itens (quantificação universal).
A ideia da quantificação existencial é implementada identificando “todas as atribuições de variáveis” que satisfazem uma condição de consulta [10].
Embora possa parecer abstrato, o conceito será discutido em um contexto mais prático nas próximas seções.
A quantificação existencial é um conceito desconhecido para muitos programadores, o que pode levar a erros de lógica, como veremos na seção “Armadilhas”.
Ponto de entrada
O Rego não tem uma função “main” que inicie sua execução. Em vez disso, o OPA é configurado com um ponto de entrada. Um ponto de entrada é uma string que aponta para regras Rego, por exemplo, data.mypolicies.deny. Esse ponto de entrada solicita que o OPA consulte todas as regras Rego deny no namespace mypolicies, combine os resultados de todas as regras e os retorne. É possível fornecer vários pontos de entrada. Se as condições de uma regra não forem atendidas, ela será considerada “não resolvível” e não será incluída no resultado final.
Fornecer apenas um ponto de entrada não basta para executar o OPA. Também é necessário um documento input, que funciona como uma variável global no Rego e contém todos os dados de fontes externas.
Otimização
Pode parecer cedo demais falar sobre otimizações do Rego, mas vale destacar uma delas, pois pode causar um comportamento inesperado.
Essa otimização é chamada “saída antecipada na avaliação de regras” e pode ser a origem de um erro de lógica. A documentação oficial descreve essa medida com clareza e detalhes. No entanto, esse é um tópico avançado que talvez não seja considerado por quem está começando a usar o Rego. A seção “Armadilhas” aborda esse risco e sugere alternativas.
A parte delicada dessa otimização é descrita da seguinte forma:
Quando é possível aplicar a “saída antecipada” a uma regra ou conjunto de regras, as iterações dentro dessa regra são canceladas assim que uma atribuição satisfaz o corpo da regra:
package earlyexit.iteration
p {
some p
data.projects[p] == "project-a"
}
Como nenhuma possibilidade poderia alterar o resultado de data.earlyexit.iteration.p depois que uma atribuição de variável satisfaz as condições, não ocorre mais nenhuma iteração. – documentação do OPA
Em outras palavras, o OPA para de percorrer data.projects assim que encontra um item igual à string project-a, uma consequência direta da quantificação existencial. Isso faz sentido e está correto; ainda assim, é importante estar ciente desse comportamento.
Igualdade
O Rego tem três tipos de operadores de igualdade, cada um com um significado diferente:
operador de igualdade | significado |
|---|---|
| operador de comparação, usado para comparar valores de variáveis |
| operador de atribuição, usado para atribuir valores a variáveis |
| operador de unificação, usado para combinar atribuição e comparação |
O operador = faz primeiro a atribuição e depois a comparação. Se a atribuição falhar, o OPA compara os valores mesmo assim:
# input
[1, 2, 5]
# code
default allow := false
allow {
input[_] = 5 # assignment fails, comparison evaluates to `true`
# (`input[_] := 5` creates error "cannot assign to ref")
}
# output
{
"allow": true
}
A menos que haja um motivo para usar o operador `=`, recomendamos evitá-lo [13]. Em vez disso, use o operador `:=` para atribuições e o operador `==` para comparações.
Políticas, regras e funções
Antes de avançar para esta seção, é útil conhecer as estruturas de dados do Rego: valores escalares, strings, valores compostos e compreensões.
Políticas
Uma política é um conceito de nível mais alto no Rego, que se refere a um conjunto de regras que define o comportamento e as restrições de um sistema. Por exemplo, o documento a seguir é uma política que contém as regras Rego allow e deny:
package mypolicy
allow {
input.name == "bar"
}
deny {
input.name == "foo"
}
Regras
As regras produzem os chamados “documentos virtuais”, estruturas de dados calculadas em tempo de execução. Saiba mais sobre regras aqui. Por exemplo:
# input
[
"world-a",
"universe-a",
"world-b",
"universe-b"
]
# code
worlds[world] {
item := input[_] # loop over `input`
startswith(item, "world")
world := item # var `world` is return value
}
universes[universe] {
item := input[_]
startswith(item, "universe")
universe := item
}
# output
{
"universes": [
"universe-a",
"universe-b"
],
"worlds": [
"world-a",
"world-b"
]
}
Se nenhum valor de retorno for definido, as regras Rego retornam true, conforme indicado na documentação: > Se o value for omitido, o valor padrão será true. – documentação
Portanto, as duas regras a seguir têm o mesmo comportamento:
deny := true if { # rule header
true # rule body
}
deny { # optimized rule header
true
}Como mencionado anteriormente, as regras só concluem a execução se todas as condições do corpo forem atendidas. Por isso, a regra a seguir não retorna nada:
allow := true {
false
}As regras que retornam dados do tipo set ou object são chamadas de regras incrementais e usam uma sintaxe um pouco diferente:
# input
[
"a",
"b",
"c",
"c"
]
# code
return_set[item] {
item := input[_]
}
return_object[key] := value {
value := input[key]
}
# output
{
"return_object": {
"0": "a",
"1": "b",
"2": "c",
"3": "c"
},
"return_set": [
"a",
"b",
"c"
]
}
As regras Rego não aceitam parâmetros. Por exemplo, return_set(param1 é um código de regra inválido), mas você pode usar [funções][#functions] no lugar.
Quantificação existencial
O comportamento do OPA na execução de regras é bem definido: ao avaliar os corpos das regras, o OPA procura atribuições de variáveis que tornem verdadeiras todas as expressões. – documentação
Como funciona a quantificação universal em linguagens imperativas? Veja um exemplo em Python:
for n in numbers: # think "FOR ALL" items `n` in numbers
if n % 2 == 0:
print(n)Este trecho de código encontra os números pares na matriz numbers. A mesma tarefa também pode ser feita no Rego. Por exemplo:
even_numbers[n] {
n := input[_]
n % 2 == 0 # think "FOR ANY" item `n` in inputs
}No exemplo acima, a palavra-chave some é usada para declarar a variável n. O OPA tenta encontrar qualquer número n da matriz input que satisfaça a condição de ser par. Se for o caso, a atribuição da variável n será retornada.
Ao identificar todas as atribuições de variáveis correspondentes, o OPA determina se a quantificação existencial foi satisfeita.
Embora não seja obrigatório, declarar variáveis explicitamente usando some pode ajudar a deixar o código mais claro e compreensível.
Outro exemplo: encontrar todos os valores de um objeto que começam com a string 1:
# input
{
"a": "1a",
"b": "2b",
"c": "1c"
}
# code
prefixed[item] {
item := input[_]
startswith(item, "1")
}
# output
{
"prefixed": [
"1a",
"1c"
]
}A linha item := input[_] percorre o documento de entrada. O segundo item de entrada, b, não satisfaz a condição da regra startswith(l, "1"). O OPA não interrompe a execução do loop, mas ignora o resultado da iteração e passa para o próximo item, c, para continuar “procurando todas as atribuições de variáveis”.
Esse comportamento é útil, mas pode causar um erro de lógica.
Funções
As funções no Rego se parecem com regras, mas têm um comportamento diferente:
greet(name) := msg {
msg := sprintf("Hello %s!", [name])
}
greeting := greet("Bob") # returns "Hello Bob!"Não é possível usar uma função sem parâmetros, pois essa definição é interpretada como uma regra:
greet() := msg {
msg := "Hello"
}
greet() # creates error "rego_parse_error:
# rule argument list must take at least one argument"Como as regras representam documentos virtuais e as funções não, elas precisam ser acessadas de formas diferentes:
# input
[
[1, 1],
[2, 2],
[3, 3]
]
# code
add_function(inp) := result { # define function
result := [ sum | # create array comprehension
arr := inp[_]
sum := arr[0] + arr[1]
]
}
output := add_function(input) # store result of function
dummy_rule_iterate_function_output[item] {
item := output[index] # work with function result
}
add_rule = result { # define rule
result := [ sum | # create array comprehension
arr := input[_]
sum := arr[0] + arr[1]
]
}
dummy_rule_iterate_rule[item] {
item := add_rule[index] # work with rule result
}
# output
"add_rule": [
2,
4,
6
],
"output": [
2,
4,
6
]
Tanto regras quanto funções podem ser usadas para compartilhar funcionalidades entre políticas.
Fluxo de controle
Noções básicas
Para criar uma lógica que verifique várias condições, basta listar cada uma em uma linha:
# input
{
"name": "foo"
}
# code
my_rule {
startswith(input.name, "f") # first condition (AND..)
endswith(input.name, "o") # second condition
}
# output
{
"my_rule": true
}
Para criar um fluxo de controle com diferentes cenários em que uma regra deve ser avaliada como true, basta definir a regra várias vezes (o cabeçalho da regra deve ser igual em todas as definições). Se alguma das regras for avaliada como true, o valor de retorno será true. Se as regras retornarem coleções, será retornada a união de todos os itens cujas condições foram atendidas.
No exemplo a seguir, as regras my_rule retornam um valor booleano:
# input
{
"name": "foo"
}
# code
my_rule {
count(input.name) == 3
}
my_rule {
input.name == "foobar" # not true; aborts execution
}
# output
{
"my_rule": true
}A estrutura if/else pode ser implementada de forma semelhante às linguagens imperativas usando a palavra-chave else:
# input
{
"name": "foo"
}
# code
my_rule := msg {
input.name == "foo"
msg := "Hello foo!"
} else := msg {
msg := "Hello anonymous"
}
# output
{
"my_rule": "Hello foo!"
}A lógica de negação é implementada usando a palavra-chave not:
# input
{
"a": "1a",
"b": "2b",
"c": "1c"
}
# code
prefixed[item] {
item := input[_]
not startswith(item, "1")
}
# output
{
"prefixed": [
"2b"
]
}O operador not é usado para negação lógica, enquanto o operador != é usado para comparar valores e verificar se são diferentes.
Laços
Em Rego, os laços são chamados de iterações e estão bem documentados. No entanto, geralmente não são necessários, pois usa-se a quantificação existencial.
Ambiguidade de sintaxe
Rego usa a mesma sintaxe para iterar sobre valores e acessá-los em conjuntos, objetos e arrays. Por isso, é essencial estar ciente das estruturas de dados envolvidas. O comportamento do fluxo de controle muda de acordo com a estrutura de dados, mesmo quando se usa a mesma sintaxe:
# input
{
"array": [0, 3, 5],
"object": {"foo": "bar"}
}
# code
iterate_array[msg] { # returns array of strings
value := input.array[index] # loop over array
msg := sprintf("%d: %d", [index, value])
}
access_key_in_object := v { # returns string
v := input.object["foo"] # get value by key "foo"
}
create_set := s {
s := { v |
v := input.array[_]
}
}
iterate_set[v] { # returns array of integers
create_set[v] # iterate over set
}
check_key_in_object { # returns `true`
input.object["foo"] # check if key "foo" exists
}Sem contexto, nem sempre é possível deduzir a semântica do código Rego apenas lendo uma instrução.
Exemplo 1:
var3 := var1[var2]
Se a estrutura de dados de var1 for um…
| então |
| então |
Exemplo 2:
var1[var2]
Se a estrutura de dados de var1 for um(a)…
| então |
| então será verificado se a chave |
| então |
Armadilhas
Defeitos aparecem em qualquer base de código. Automatizar a detecção e a prevenção é uma medida eficaz que deve fazer parte de qualquer processo de desenvolvimento em Rego.
Duas ferramentas úteis para isso:
O OPA inclui o comando integrado
check, que ajuda a identificar erros de compilação e problemas no código.
A Styra, empresa por trás do OPA, lançou o
regal, um linter para Rego que analisa o código-fonte em busca de vários problemas, desde estilo de código até bugs.
Otimização de saída antecipada
Como discutido na seção “Otimização”, o Rego encerra a execução antes de executar todo o código se conseguir determinar o resultado final.
Imagine que alguém queira criar uma política que só permita o acesso se todas as conexões do usuário com a empresa tiverem origem em uma de suas filiais:
# input
[
"on-prem",
"remote",
"on-prem",
"on-prem"
]
# code
default allow := false
allow {
input[_] == "on-prem"
}
# output
{
"allow": true
}Essa política retorna true, apesar de uma das conexões vir de uma origem remota. Esse comportamento pode surpreender programadores que não conhecem programação lógica.
Por exemplo, em Python, o código poderia ser semelhante ao exemplo abaixo e funcionar como esperado:
origins = ['on-prem', 'remote', 'on-prem', 'on-prem']
def allow():
for origin in origins:
if origin != 'on-prem':
return 1
return 0
if __name__ == '__main__':
raise SystemExit(allow())O bug lógico está na otimização de saída antecipada do Rego: assim que o Rego consegue avaliar como true as condições da regra allow, ele para. Isso acontece porque a quantificação existencial é satisfeita quando a origem é associada a on-prem na primeira iteração. Isso é bem diferente da programação imperativa, na qual a condição é avaliada para cada item de origins antes de se tomar uma decisão.
É preciso aplicar a quantificação universal (FOR ALL) para corrigir o bug.
Uma opção de correção é usar uma comprehension:
default allow := false
allow {
all_allows := [ allowed |
origin := input[_]
origin == "on-prem"
allowed := true
]
count(all_allows) == count(input)
}Outra opção de correção é usar a palavra-chave every (introduzida no OPA v0.38.0) [14] [15]:
import future.keywords.every
default allow := false
allow {
every origin in input {
origin == "on-prem"
}
}Uma terceira opção é inverter a lógica da regra usando negação:
# input
[
"remote",
"on-prem",
"on-prem"
]
# code
default deny := false
deny {
input[_] != "on-prem"
}
# output
{
"deny": true
}Essa regra deny retornará true, como esperado, se alguma das conexões do usuário tiver origem fora de um escritório da empresa.
No entanto, usar a palavra-chave every é a opção mais adequada, pois ela impõe intencionalmente a quantificação universal e, assim, expressa as intenções de quem criou a política de forma mais clara e consciente [14].
Sempre que criar uma regra que contenha uma iteração, quem cria a política deve refletir se a quantificação universal ou existencial produz o comportamento desejado.
Valores indefinidos
A seção “Introdução”, acima, explica que a execução do código é interrompida se uma linha não for avaliada como true. Em outras palavras, quando uma regra lógica não pode ser resolvida porque sua condição não foi atendida, ela não retorna false, como programadores acostumados a linguagens imperativas poderiam esperar; a execução do código simplesmente para. Isso também se aplica a valores ou atributos undefined [17].
Por exemplo, o código Rego é avaliado como undefined ao acessar um namespace ou atributo de objeto inexistente.
Isso pode levar a comportamentos indesejados e ser difícil de depurar:
# input
{
"message": "world"
}
# code
deny {
input.mesage == "world"
}
# output
{}O atributo input.mesage contém um erro de digitação e não existe. O Rego só sabe em tempo de execução se esse atributo existe ou é undefined; por isso, o OPA simplesmente para silenciosamente ao avaliar essa linha.
Para investigar esse tipo de problema, costuma ser útil incluir várias instruções print() na base de código para localizar a linha com erro. Lembre-se também de verificar os namespaces dos pacotes.
Outra forma de identificar a linha com erro é desativar (comentar) grandes partes da base de código e reativá-las passo a passo até encontrar o problema.
“true” em JSON
O documento input enviado ao OPA contém dados formatados em JSON, que aceita estruturas de dados booleanas e strings [18]. Na prática, valores booleanos podem ser representados como strings (por exemplo, "true"), o que pode levar a decisões de política indesejadas. Por exemplo:
# input
{
"privileged": "true"
}
# code
deny {
input.privileged == true
}
# output
{}Uma possível solução é criar uma função auxiliar que também considere strings:
1# input
2{
3 "privileged": "true"
4}
5
6# code
7is_true(b) := ret {
8 is_boolean(b)
9 b
10 ret := b
11} else := ret {
12 b == "true"
13 ret := true
14} else = false {
15 true
16}
17
18deny {
19 is_true(input.privileged)
20}
21
22# output
23{
24 "deny": true
25}Testes
Os testes são essenciais para garantir que as políticas se comportem como esperado. O OPA oferece o comando test, que facilita a execução de testes.
Considerando a política a seguir (arquivo policy.rego):
package mypolicy
deny {
input.privileged == true
}É simples implementar testes (arquivo policy_tests.rego):
package mypolicy
test_deny_privileged {
deny with input as {"privileged": true}
}
test_deny_unprivileged {
not deny with input as {"privileged": false}
}O comando ./opa test policy.rego policy_tests.rego executa os testes e exibe o resultado: PASS: 2/2.
Depuração
A depuração é importante em qualquer projeto de programação. O OPA oferece um REPL e o comando eval, que podem ser usados para depurar. No entanto, ainda não existe um depurador típico, como o gdb, que permita percorrer o código passo a passo e inspecionar símbolos. Por isso, imprimir valores de variáveis continua sendo uma abordagem importante para depurar.
Neste exemplo, os testes do capítulo anterior, “Testes”, são ampliados com um caso de teste que executa a política com a entrada "true":
test_deny_string {
deny with input as {"privileged": "true"}
}Como esperado, o caso de teste falha:
$ ./opa test policy.rego policy_test.rego
policy_test.rego:
data.mypolicy.test_deny_string: FAIL (85.375µs) --------------------------------------------------------------------------------
PASS: 2/3
FAIL: 1/3
Para investigar o problema, é possível exibir o valor de input.privileged usando print():
deny {
print(sprintf("value of `privileged`: %v", [input]))
input.privileged == true
}Ao executar o caso de teste novamente, o valor é exibido:
$ ./opa test policy.rego policy_test.rego
policy_test.rego:
data.mypolicy.test_deny_string: FAIL (88.875µs)
value of `privileged`: {"privileged": "true"} --------------------------------------------------------------------------------
PASS: 2/3
FAIL: 1/3
A saída agora revela que input.privileged é uma string, e não um booleano, e permite corrigir o problema.
O OPA oferece a função print() desde a versão v0.34.0. Felizmente, print() não para diante de valores undefined; em vez disso, exibe <undefined>.
Uma ferramenta útil de terceiros para depurar código, testar políticas e muito mais é o fregot.
Erros comuns
Complete rules must not produce multiple outputs
Esse erro ocorre quando uma ou mais regras da mesma definição recebem a mesma entrada e produzem saídas diferentes. Por exemplo:
# input
-
# code
a_rule := res {
res := "b"
}
a_rule := res {
res := "c"
}
# outputpolicy.rego:7: eval_conflict_error: complete rules must not produce multiple outputs
A primeira definição de a_rule retorna "b", enquanto a segunda retorna "c", resultando em um comportamento não determinístico.
Muitas vezes, todos os valores de retorno são válidos e o resultado esperado é a união deles. Isso pode ser feito retornando um conjunto:
# input
-
# code
a_rule[res] {
res := "b"
}
a_rule[res] {
res := "c"
}
# output
{
"a_rule": [
"b",
"c"
]
}Outra opção comum é combinar as duas regras e retornar uma coleção. Se nenhuma dessas opções levar ao resultado desejado, talvez seja necessário revisar a lógica da política.
OPA em caminhos críticos
Rego e OPA são ferramentas confiáveis para criar e avaliar políticas. As garantias de decidibilidade e terminação trazem segurança e permitem criar estruturas de autorização avançadas, que podem fazer parte de qualquer caminho crítico de aplicações empresariais.
No entanto, usar Rego tem um custo, pois muitos programadores não conhecem alguns dos conceitos envolvidos em programação lógica, como a quantificação existencial ou a otimização de saída antecipada do OPA, discutida na seção “Introdução”.
Como consequência, os programadores precisam enfrentar uma curva de aprendizado inicialmente íngreme, que funciona como uma barreira de entrada. Além disso, considerando as possíveis armadilhas, o custo de adotar OPA deve ser avaliado e comparado ao de outras abordagens, como escolher uma linguagem de programação imperativa, que conta com um ecossistema de ferramentas para desenvolvimento mais maduro e muito mais profissionais disponíveis (claro, linguagens imperativas também não estão livres de armadilhas).
Por exemplo, ao criar um software em que o risco de efeitos colaterais é aceitável e o tempo para chegar ao mercado é essencial, a decisão de escolher Rego deve levar em conta suas consequências.
Priorize o que mais importa
As organizações precisam de uma abordagem holística para priorizar riscos. Use a priorização contextual baseada em riscos da Snyk.