Skip to main content

Rego 102: Abfragen mit AND/OR und benutzerdefinierten Meldungen kombinieren

feature cloud security

9. November 2023

0 Min. Lesezeit

Diese Blogpost-Reihe bietet eine verständliche Einführung in Rego, die Policy-Sprache der Entwickler der Open Policy Agent (OPA)-Engine. Wenn Sie noch am Anfang stehen und Rego-Policies als Code schreiben möchten, sind Sie hier genau richtig.

In dieser dreiteiligen Reihe behandeln wir folgende Themen:

Zur Erinnerung: Rego ist eine deklarative Abfragesprache von den Entwicklern des Open Policy Agent (OPA)-Frameworks. Die Cloud Native Computing Foundation (CNCF) nahm OPA im April 2019 als gehostetes Projekt auf Inkubationsebene auf. 2021 schloss OPA die Inkubationsphase ab.

Mit Rego schreiben Sie Policies als Code. Dabei kommen Programmierpraktiken wie Versionskontrolle und modularer Aufbau zum Einsatz, um Cloud- und Infrastructure-as-Code-Ressourcen (IaC) auszuwerten. OPA ist die Engine, die in Rego geschriebenen Policy-Code auswertet. Snyk verwendet die Rego-Sprache für benutzerdefinierte Regeln.

Rückblick auf Teil 1

In Teil 1 dieser Blogpost-Reihe haben wir erklärt, dass eine Rego-Regel eine bedingte Zuweisung ist. Eine Regel fragt die Eingabe nach einer passenden Bedingung ab. Wird eine Übereinstimmung gefunden, wird einer Variablen ein Wert zugewiesen.

Eine Regel können Sie so lesen:

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

Hier ist das Beispiel, das wir verwendet haben. Es beschreibt eine Unternehmens-Policy: Nur Alice, eine Netzwerkadministratorin, darf virtuelle Netzwerke im Prod-Konto erstellen und löschen.

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

OPA wertet ein JSON- oder YAML-Eingabedokument anhand einer Regel aus, um eine Policy-Entscheidung zu treffen. Das folgende Eingabedokument enthält den aktuell angemeldeten Benutzer:

{
  "user": "alice"
}

Wenn Sie mit OPA die Eingabe anhand dieser Regel auswerten, findet OPA eine Übereinstimmung für die Abfrage input.user == "alice". Daher erhält die Variable im Regelkopf, allow, den Wert aus dem Regelkopf: true. Das folgende Ergebnis belegt dies:

{
  "allow": true
}

OPA hat entschieden, dass Alice, die aktuell angemeldete Benutzerin, virtuelle Netzwerke im Prod-Konto erstellen und löschen darf. Die Eingabe ist mit der Regel konform.

AND und OR

Bisher haben wir nur Regeln mit einer einzelnen Abfrage gezeigt. Eine Regel kann auch mehrere Abfragen enthalten. In diesem Fall stehen die Abfragen für mehrere Bedingungen, die alle erfüllt sein müssen, damit einer Variablen ein Wert zugewiesen wird. Implizit gilt ein AND – „Diese Bedingung muss erfüllt sein UND diese Bedingung muss erfüllt sein.“

In der folgenden Regel müssen beispielsweise sowohl input.user == "alice" UND input.environment == "prod" wahr sein, damit der Variablen allow der Wert true zugewiesen wird:

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

In manchen Fällen ist ein OR passender. Ein OR lässt sich darstellen, indem Sie in mehreren Regeln denselben Regelkopf verwenden:

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

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

Diese Regeln lassen sich wie folgt lesen:

allow ist true, wenn user "alice" ist ODER wenn user "bob" ist.

Technisch gesehen bilden die Regeln zusammen eine einzige Regel, da beide denselben Regelkopf haben. Weil Sie diese Regel in mehreren Schritten definieren, wird sie als inkrementelle Regel bezeichnet.

Sie können den zweiten Regelkopf auch weglassen und die Regelkörper zusammenführen. Die obige Regel lässt sich kürzer so schreiben:

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

Vielleicht fragen Sie sich, warum wir den Unifikationsoperator = statt des Zuweisungsoperators := verwendet haben. Das liegt daran, dass Variablen in Rego unveränderlich sind. Auch wenn Regeln mit demselben Regelkopf als eine inkrementelle Regel behandelt werden, würden Sie bei Verwendung des Zuweisungsoperators dieselbe Variable effektiv mehrfach „zuweisen“ – und das ist in Rego nicht erlaubt. Stattdessen verwenden wir im Regelkopf den Unifikationsoperator, weil er mehrere Regeln mit demselben Namen vereinheitlicht.

Wenn Sie sich nur schwer merken können, wann welcher Operator im Regelkopf verwendet wird, gibt es dank Standardwerten und etwas syntaktischer Vereinfachung eine einfachere Möglichkeit.

Standardwerte in Regelköpfen

Der Standardwert einer Variablen im Regelkopf ist true. Deshalb bietet Rego hier eine syntaktische Vereinfachung: Weist eine Regel der Variablen den Wert true zu, können Sie := true im Regelkopf weglassen.

Das bedeutet, dass diese AND-Regel …

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

… dasselbe ist wie diese AND-Regel:

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

Und genauso ist diese OR-Regel …

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

… dasselbe wie diese OR-Regel:

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

„Klasse“!

Das Schlüsselwort default

Wie wir in Teil 1 besprochen haben, wird der Variablen im Regelkopf der dort angegebene Wert nicht zugewiesen, wenn die Eingabe keine Übereinstimmung für eine Regelabfrage enthält.

Zur Veranschaulichung kehren wir zu unserer Beispielregel zurück: Nur Alice, eine Netzwerkadministratorin, darf virtuelle Netzwerke im Prod-Konto erstellen und löschen.

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

Nehmen wir an, wir haben ein Eingabedokument, in dem Bob als aktuell angemeldeter Benutzer angegeben ist:

{
  "user": "bob"
}

Da input.user nicht "alice" ist, findet OPA bei der Auswertung der Regel anhand der Eingabe keine Übereinstimmung. Daher erhält allow nicht den Wert true, und das Ergebnis der Auswertung ist eine leere Menge:

{}

In diesem Fall sagen wir, dass der Wert von allow undefiniert ist. Wenn OPA Eingaben zur Auswertung einer Regel abfragt, gibt es nur passende Werte zurück. Gibt es keinen passenden Wert, gibt es nichts zurück – daher die leere Menge.

Was, wenn wir möchten, dass OPA false zurückgibt, wenn allow nicht ausdrücklich true ist? Mit dem Schlüsselwort default können wir einen Standardwert festlegen. Das bedeutet: Wenn eine Regel nicht ausdrücklich zu true ausgewertet wird, gibt sie einen bestimmten Wert zurück (in diesem Fall false), statt eine leere Ergebnismenge zurückzugeben. Dazu schreiben wir eine zusätzliche Regel, die ebenfalls allow verwendet:

default allow = false

Wenn OPA nun feststellt, dass input.user nicht "alice" ist, ergibt die Auswertung von allow keine leere Menge. Stattdessen erhält die Variable den Standardwert, den wir als false festgelegt haben:

{
  "allow": false
}

Beachten Sie: Wenn Sie das Schlüsselwort default angeben, verwenden Sie sowohl in der Regel, in der Sie den Standardwert definieren, als auch in der Regel für die bedingte Zuweisung den Unifikationsoperator = statt des Zuweisungsoperators :=. Auch hier gilt: Variablen sind unveränderlich. Verwenden Sie den Zuweisungsoperator, weisen Sie dieselbe Variable mehrfach zu – das lässt Rego nicht zu. Verwenden Sie stattdessen den Unifikationsoperator:

default allow = false

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

Natürlich können Sie auch die zuvor beschriebene syntaktische Vereinfachung nutzen und = true in der Regel mit der bedingten Zuweisung weglassen. Das ist völlig in Ordnung und möglicherweise einfacher, weil Sie sich nicht merken müssen, welcher Operator für die bedingte Zuweisung zu verwenden ist:

default allow = false

allow {
  input.user == "alice"
}

Benutzerdefinierte Meldungen

Manchmal möchten Sie statt eines einfachen Ergebnisses wie bestanden oder nicht bestanden – true oder false/undefiniert – mehrere Meldungen zurückgeben. Dafür verwenden Sie den Regelkopf deny[msg] und weisen der Variablen msg die gewünschte Meldung zu. Die folgende Regel prüft, ob der Benutzer nicht Alice ist. Falls nicht, weist sie msg die Zeichenfolge „User is denied access“ zu, die dann der Menge deny hinzugefügt wird (mehr zu Mengen erfahren Sie in Teil 3):

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

Um dies zu testen, nehmen wir an, unser Eingabedokument enthält den Namen des aktuell angemeldeten Benutzers:

{
	"user": "bob"
}

Wenn wir die Regel anhand der obigen Eingabe mit dem Rego Playground von OPA oder dem Befehl opa eval -i input.json -d check_user.rego "data.rules.check_user" --format pretty auswerten (Anweisungen finden Sie in unserem Blogpost zu Teil 1), sieht das Ergebnis so aus:

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

Was passiert hier? Wir erstellen eine Mengenregel, auf die wir in unserem nächsten Blogpost dieser Reihe zurückkommen. Für den Moment genügt es zu wissen, dass wir statt eines einzelnen Ergebnisses – true, false oder undefiniert – eine Menge von Meldungen zurückgeben, die der Variable deny zugewiesen sind. In Rego ist eine Menge eine ungeordnete Liste eindeutiger Elemente, etwa Ganzzahlen { 1, 2, 3 }, Zeichenfolgen { "alice", "bob", "carlotta" } oder sogar andere Mengen { { 1, 2}, {3, 4} }. Sie können eine Menge aus jedem unterstützten Rego-Typ bilden und sogar verschiedene Typen in einer Menge kombinieren.

In unserer Beispielregel ist jedes Element der Menge deny eine Zeichenfolge mit einer Meldung. Für dieses Eingabedokument gibt es nur ein Element in der Menge, es können aber auch mehr sein. Ein Beispiel dazu zeigen wir später in diesem Blogpost.

Was, wenn wir in der Meldung weitere Informationen zurückgeben möchten? Mit der integrierten Funktion sprintf können wir den Wert des Felds input.user anzeigen, der das Ergebnis deny ausgelöst hat:

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

Die Funktion sprintf nimmt zwei Argumente entgegen: eine Zeichenfolge und ein Array mit Werten. In diesem Fall enthält das Array nur ein Element: die Zeichenfolge input.user. Im ersten Argument verwenden wir %v als Platzhalter. Bei der Auswertung der Regel wird er durch den Wert im Array ersetzt.

Wenn wir die Regel nun mit der folgenden Eingabe auswerten …

{
	"user": "bob"
}

… sehen wir dieses Ergebnis:

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

Das Schlüsselwort not

Sie können einen Ausdruck negieren, indem Sie ihm das Schlüsselwort not voranstellen. Dadurch erhält er die gegenteilige Bedeutung. Meistens verwenden Sie dies in einer Abfrage, um anzugeben, dass eine Eigenschaft in der Eingabe fehlt. Diese Abfrage etwa:

input.tags.environment

… bedeutet: „Das Eingabedokument enthält die Eigenschaft tags.environment, und ihr Wert ist nicht false.“ Diese Abfrage dagegen:

not input.tags.environment

… bedeutet: „Das Eingabedokument enthält keine Eigenschaft tags.environment, oder tags.environment ist auf false gesetzt.“ Es gibt keine Überschneidung und keinen Mittelweg: Ein Ausdruck und seine Umkehrung schließen sich gegenseitig aus.

Hier ist eine Beispielregel, die true an deny zuweist, wenn der Eingabe das Tag department fehlt:

deny {
  not input.tags.department
}

Verwenden wir diese Eingabe:

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

Würden wir diese Eingabe anhand der obigen Regel auswerten, würden wir sehen, dass deny true zurückgibt, weil die erforderliche Eigenschaft department fehlt:

{
  "deny": true
}

Eine Beispielregel mit OPA auswerten

Probieren wir die in diesem Blogpost besprochenen Konzepte aus und werten eine Beispielregel aus. Wie in Teil 1 konzentrieren wir uns auf zwei Möglichkeiten, mit OPA zu arbeiten:

  • Den Rego Playground verwenden

  • Das OPA-Kommandozeilentool verwenden

Eine Anleitung zur Verwendung dieser Schnittstellen finden Sie in Teil 1.

Diesmal verwenden wir ein praxisnäheres Beispiel mit einem Kubernetes-Pod. Hier ist das JSON-Manifest, das wir als Eingabe verwenden:

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

Und hier ist die Regel, anhand derer wir die Eingabe auswerten. Sie setzt die Unternehmens-Policy „Kubernetes-Pods müssen mit release und environment gekennzeichnet sein“ durch:

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])
}

Diese Regel veranschaulicht einige der Konzepte, die wir in diesem Blogpost besprochen haben:

  • deny[msg], um eine Menge benutzerdefinierter Meldungen statt true oder false/undefined zurückzugeben

  • Die Regelstruktur kombiniert AND und OR:

    • Ablehnen, wenn es sich beim Kubernetes-Objekt um einen Pod handelt UND ihm das Release-Label fehlt ODER:

    • Ablehnen, wenn es sich beim Kubernetes-Objekt um einen Pod handelt UND ihm das Environment-Label fehlt

  • Das Schlüsselwort not, um das Fehlen einer Eigenschaft zu prüfen

  • Die Funktion sprintf, um eine Meldung mit dem Namen des nicht konformen Pods zurückzugeben

Damit Sie es einfacher haben, haben wir einen Playground mit diesen Inhalten vorbereitet: https://play.openpolicyagent.org/p/KNVK9kEvIT

Wenn Sie die Regel auswerten, indem Sie im Playground auf Evaluate klicken, oder lokal OPA mit einem Befehl wie opa eval -i input.json -d check_pod.rego "data.rules.check_pod" --format pretty ausführen, sehen Sie folgende Ausgabe:

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

Wie wir sehen, entspricht der überprüfte Kubernetes-Pod nicht unserer Regel, da die Eingabe keine Eigenschaft labels.environment enthält.

Entfernen wir nun die Eigenschaft labels.release. Der Abschnitt labels der Eingabe sollte dann so aussehen:

    "labels": {
    }

Wenn Sie die Regel jetzt auswerten, sehen Sie, dass das deny-Set zwei Meldungen enthält:

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

Fügen wir der Eingabe schließlich sowohl eine Eigenschaft labels.release als auch eine Eigenschaft labels.environment hinzu, sodass sie so aussieht:

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

Was passiert, wenn wir die Regel erneut auswerten? Das deny-Set ist leer:

{
  "deny": []
}

Das bedeutet, dass unser Pod regelkonform ist, da OPA keine Meldungen zum deny-Set hinzugefügt hat. Hurra!

Wie geht es weiter?

Kehren Sie zu unserem Blog zurück und lesen Sie Rego for Beginners Part 3. Darin beschäftigen wir uns mit Set-Regeln, Objektregeln, Funktionen und Iterationen.

In der Zwischenzeit finden Sie hier einige nützliche Ressourcen:

Wenn Sie Rego verwenden möchten, um benutzerdefinierte Regeln für Snyk IaC zu schreiben, finden Sie hier unsere Dokumentation. Zusätzlich zu den integrierten Sicherheits- und Compliance-Regelsätzen von Snyk ermöglichen Ihnen benutzerdefinierte IaC+-Regeln, Sicherheitskontrollen für Ihren gesamten SDLC festzulegen.

IaC+ bietet Ihnen eine zentrale Übersicht über Ihre Konfigurationsprobleme und ermöglicht deren Kontrolle – vom Code bis zur Cloud. Dafür stehen Ihnen eine Benutzeroberfläche für Probleme, ein Regelsatz und eine Policy-Engine zur Verfügung, die IDE, SCM, CLI, CI/CD, Terraform Cloud und bereitgestellte Cloud-Umgebungen wie AWS, Azure und Google Cloud abdecken.

Weiterlesen

feature customer snowflake
Article

Sicherheit von Anfang an: Die Snyk Studio-Integration für Snowflake Cortex Code

Snyk Studio ist in Snowflake Cortex Code integriert und scannt KI-generierten Code, Abhängigkeiten und Container während der Entwicklung auf Schwachstellen.

blog feature pypi spoof
Article

Die gesamte Snyk AI Security Platform – kostenlos für Open-Source-Maintainer

Open-Source-Maintainer werden von echten Schwachstellenmeldungen überflutet und brauchen Unterstützung dabei, diese zu priorisieren, zu beheben und schneller Korrekturen bereitzustellen. Das Secure Developer Program von Snyk bietet qualifizierten Projekten kostenlosen Zugang zur Snyk AI Security Platform.

blog feature snyk code green
Blog

Sicherheit auf Typebene: Die Zukunft der sicheren KI-Codegenerierung?

Sicherheitsorientierte Typen können häufige Fehler zu Compile-Time-Fehlern machen. In diesem Beitrag erfahren Sie, wie Typsicherheit dazu beitragen könnte, ganze Klassen KI-generierter Schwachstellen zu verhindern.