Skip to main content

Rego 103: Wert- und Regeltypen

feature cloud security

16. November 2023

0 Min. Lesezeit

Diese Blogpost-Reihe bietet eine verständliche Einführung in Rego, die Richtliniensprache der Entwickler der Open Policy Agent (OPA)-Engine. Wenn Sie gerade erst anfangen und Rego-Richtlinien als Code schreiben möchten, sind Sie hier richtig.

In dieser dreiteiligen Reihe behandeln wir die folgenden Themen:

  • Teil 1: Rego 101: Einführung in Rego

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

  • Teil 3 (dieser Teil!): Wert- und Regeltypen

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

Rego wird verwendet, um Richtlinien als Code zu schreiben. Dabei kommen Programmierpraktiken wie Versionskontrolle und modulares Design bei der Auswertung von Cloud- und Infrastructure-as-Code-Ressourcen (IaC) zum Einsatz. OPA ist die Engine, die Richtlinien als in Rego geschriebenen Code auswertet. Snyk verwendet die Sprache Rego für benutzerdefinierte Regeln.

Rückblick auf Teil 2

In Teil 2 haben wir Ihnen gezeigt, wie Sie Folgendes verwenden:

  • AND- und OR-Regeln

  • Das Schlüsselwort default

  • Das Schlüsselwort not

  • Benutzerdefinierte deny-Meldungen

In diesem Teil schließen wir die Reihe ab und konzentrieren uns auf Set-Regeln, Objektregeln, Funktionen und Iteration.

Werttypen

In Rego ist ein Wert die Darstellung einer bestimmten Art von Daten. Jeder Wert hat einen bestimmten Typ. Rego-Typen fallen in zwei Kategorien: Skalarwerte und zusammengesetzte Werte.

Skalarwerte stehen für eine einzelne Dateneinheit und umfassen die folgenden Typen:

  • Zeichenfolgen stehen in doppelten Anführungszeichen.

  • Zahlen umfassen positive und negative Ganzzahlen sowie Dezimalzahlen.

  • Boolesche Werte können nur true oder false sein.

  • null steht für das Fehlen eines Werts.

Zusammengesetzte Werte stehen für eine Sammlung von Werten und umfassen die folgenden Typen:

  • Arrays

  • Objekte

  • Sets

Wenn Sie unsere Blogpost-Reihe bisher verfolgt haben, kennen Sie bereits mehrere Beispiele für Skalarwerte und zusammengesetzte Werte. Zusammengesetzte Werte sind etwas komplexer, deshalb sehen wir sie uns genauer an.

Zusammengesetzte Werte

Arrays sind geordnete Listen aus einem oder mehreren Werten in eckigen Klammern. Ein Array kann Zeichenfolgen, Zahlen, andere Arrays, unterschiedliche Typen und vieles mehr enthalten:

  • ["alice", "bob", "carlotta"]

  • [2, -5, 3.8]

  • [[1, 2, 3], [4, 5, 6]]

  • [true, "banana", 17]

Sie können auf jedes Element in einem Array zugreifen, indem Sie seinen Index oder seine Position im Array angeben. Indizes beginnen immer bei 0. Das erste Element eines Arrays hat also den Index 0, das zweite den Index 1 und so weiter – wie im folgenden users-Array zu sehen ist:

users := ["alice", "bob", "carlotta"]
             0       1        2 

Wenn Sie das erste Element in der users-Liste abrufen möchten, verwenden Sie folgende Syntax:

users[0]  # evaluates to "alice"

Wenn Sie das zweite Element der Variablen admin zuweisen möchten, verweisen Sie folgendermaßen darauf:

admin := users[1]  # "bob" is assigned to admin

Wenn Sie mit der Syntax users[3] versuchen, ein (nicht vorhandenes) viertes Element in der Liste abzurufen, findet OPA keine Treffer. Die Ausgabe ist dann ein leeres Set {} (undefiniert).

Objekte sind ungeordnete Listen aus einem oder mehreren Schlüssel-Wert-Paaren in geschweiften Klammern. Schlüssel und Wert können beliebige Typen haben und müssen nicht vom selben Typ sein. Jeder Schlüssel ist durch einen Doppelpunkt mit dem zugehörigen Wert verbunden:

  • { "alice": "admin", "bob": "user", "carlotta": "user" }

  • { "ports": [80, 443] }

  • { 80: true }

Sie können auf den Wert eines Objekts zugreifen, indem Sie seinen Schlüssel angeben. Das folgende users -Objekt enthält beispielsweise drei Schlüssel-Wert-Paare:

users := { "alice": "admin", "bob": "user", "carlotta": "user" }

Um in einer Abfrage den Wert des Schlüssel-Wert-Paars mit dem Schlüssel "alice" abzurufen, verwenden Sie folgende Syntax:

users["alice"]  # evaluates to "admin"

Sets sind ungeordnete Listen aus einem oder mehreren eindeutigen Werten, ebenfalls in geschweiften Klammern. Die Werte können beliebige Typen haben:

  • {1, 2, 3}

  • {"alice", "bob", "carlotta"}

Da Sets ungeordnet sind, können zwei Sets gleich sein, wenn sie dieselben Elemente enthalten – auch wenn diese in unterschiedlicher Reihenfolge stehen. Beispielsweise ist {1, 2, 3} gleich {2, 3, 1}.

Sie können prüfen, ob ein Element in einem Set enthalten ist. Nehmen wir an, Sie haben das folgende Set:

nums := {1, 2, 3}

Um in einer Abfrage zu prüfen, ob die Ganzzahl 4 im nums-Set enthalten ist, verwenden Sie folgende Syntax:

nums[4]  # does not evaluate to true

Regeltypen

In Rego gibt es verschiedene Regeltypen:

  • Vollständige Regeln, die ein einzelnes Ergebnis liefern.

  • Regeln, die Sets erzeugen.

  • Regeln, die Objekte erzeugen.

  • Funktionen, die sich tatsächlich ein wenig von Regeln unterscheiden.

Jede dieser Regeln kann einen Abfragekörper haben. Sie unterscheiden sich darin, wie die Abfragen aufgebaut sind und welche Informationen sie zurückgeben. Im nächsten Abschnitt erklären wir die einzelnen Regeltypen, beginnend mit vollständigen Regeln.

Vollständige Regeln

Wenn Sie diese Reihe verfolgt haben, kennen Sie bereits eine vollständige Regel:

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

Eine vollständige Regel weist einer Variablen einen einzelnen Wert zu. Oben wird der Variablen allow der Wert true zugewiesen, wenn die Bedingung in der Abfrage erfüllt ist.

Hier ein weiteres Beispiel: Wenn die Bedingung in der Abfrage erfüllt ist, wird der Variablen allowed_port die Zahl 80 zugewiesen:

allowed_port := 80 {
  input.account_id != "123456789012"
}

Eine vollständige Regel kann wie oben eine Abfrage oder mehrere Abfragen enthalten, wie wir es in einem weiteren Beispiel aus Teil 2 gesehen haben:

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

Konstanten

Was ist, wenn eine Variable unabhängig von den Umständen einen bestimmten Wert enthalten soll? In anderen Programmiersprachen wird das als Konstante bezeichnet.

Sie könnten es so schreiben:

pi := 3.14 {
  true
}

Da die Bedingung in der Abfrage immer erfüllt ist, ergibt sie unabhängig von den Umständen true – die Variable pi hat immer den Wert 3.14.

Dank einer syntaktischen Vereinfachung können Sie den Regelkörper auch ganz weglassen:

pi := 3.14

Das ist eine vollständige Regel. Egal, wo Sie im Programm auf pi verweisen: Die Variable steht immer für 3.14.

Set- und Objekt-Comprehensions

Oft möchten Sie einer Variablen eine Sammlung von Werten zuweisen. Dafür gibt es Set-Comprehensions und Objekt-Comprehensions.

Set-Comprehensions

Eine Set-Comprehension fügt einer Variablen nacheinander Elemente hinzu und erzeugt so ein Set. Manchmal möchten Sie die Eingabedatei durchlaufen, um ein Set mit allen Werten zu erstellen, die bestimmte Bedingungen erfüllen. Dafür können Sie eine Set-Comprehension schreiben.

Angenommen, Sie haben dieses Eingabedokument, das ein Array aller derzeit im System angemeldeten Benutzer darstellt:

{
  "users": [
    "alice",
    "bob",
    "carlotta"
  ]
}

Erinnern Sie sich an die Unternehmensrichtlinie, der zufolge nur Alice über Administratorberechtigungen verfügt? Nehmen wir an, Sie möchten eine eindeutige Liste (ein Set) aller angemeldeten Benutzer ohne Administratorrechte erstellen.

Sie könnten ein Set folgendermaßen erstellen:

nonadmins[name] {
  name := input.users[i]
  name != "alice"
}

Zur Erklärung sehen wir uns zuerst den Regelkörper und dann den Regelkopf an.

Regelkörper: Damit weisen Sie OPA an, das input.users-Array nach Elementen zu durchsuchen, die nicht mit "alice" übereinstimmen.

Wie funktioniert das? Wie bereits erwähnt, verweisen Sie mithilfe des Index auf ein Element in einem Array. Hier steht die Variable i für den Index (Sie können sie auch anders nennen), da sie bei jedem Durchlauf durch die Eingabe erhöht wird. Wenn OPA die Abfragen ausführt, durchläuft es die users-Liste und ersetzt i durch den Index jedes Elements, um die Elemente nacheinander abzurufen. Wenn Sie mit imperativer Programmierung vertraut sind: Das ähnelt der Funktionsweise einer imperativen Schleife, ist aber nicht ganz dasselbe.

Beim ersten Durchlauf von OPA durch das users-Array in der Eingabe geschieht im Hintergrund Folgendes:

name := input.users[0]  # Evaluates to "alice"
name != "alice"  # This condition is not fulfilled, so OPA discards the username and moves on.

Beim zweiten Durchlauf geschieht Folgendes:

name := input.users[1]  # Evaluates to "bob"
name != "alice"  # This condition evaluates to true, so OPA adds the list item -- the username "bob" -- to the nonadmins set.

Beim dritten Durchlauf geschieht Folgendes:

name := input.users[2]  # Evaluates to "carlotta"
name != "alice"  # This condition also evaluates to true, so OPA adds the list item -- the username "carlotta" -- to the nonadmins set.

Regelkopf: Die Variable nonadmins bezeichnet das Set selbst, und die Variable name steht für jeden eindeutigen Namen im Set (im Regelkörper ist das wie oben erläutert input.users[i]). Wenn OPA die Logik im Regelkörper ausführt und der aktuelle Wert von name alle Bedingungen erfüllt, wird dieser Wert zum nonadmins-Set hinzugefügt.

Alles zusammen: Zusammengefasst fügt OPA bei jedem Durchlauf durch die Benutzerliste den aktuellen Wert von input.users[i] zum nonadmins[name]-Set hinzu, sofern der Wert alle in den Abfragen aufgeführten Bedingungen erfüllt.

Das Ergebnis ist dieses nonadmins-Set:

{
  "nonadmins": [
    "bob",
    "carlotta"
  ]
}

Wie Sie sehen, sind Bob und Carlotta die diensthabenden Benutzer ohne Administratorrechte!

Iteration

Ein Hinweis zur Iteration in Rego: Die Iteration erfolgt implizit. Anders als in anderen Sprachen wie Python gibt es hier keine Schleifen wie „while x == true“ oder „for y in z“. Stattdessen durchlaufen Sie ein Array, Set oder Objekt, indem Sie eine Variable anstelle eines Array-Index, Set-Elements oder Objektschlüssels verwenden. Das haben wir unten mit i im input.users-Array getan:

name := input.users[i]

Da wir eine Variable in die Klammern gesetzt haben, weiß OPA, dass wir nicht auf einen bestimmten einzelnen Wert verweisen, sondern alle Werte nacheinander betrachten.

Wenn Sie imperative Schleifen gewohnt sind, ist das zunächst eine Umstellung, aber die Syntax ist knapp! Das obige Beispiel entspricht dem folgenden Python-Ausdruck:

for i in users:
  name = i

Oder, wenn Sie es vorziehen:

for i in range(0, len(users)):
  name = users[i]

Der Unterstrich-Operator

Wenn Sie nur einmal auf den Index verweisen müssen, können Sie anstelle einer benannten Variablen wie i den Platzhalter-Operator (einen Unterstrich) verwenden:

nonadmins[name] {
  name := input.users[_]
  name != "alice"
}

Als Iterator (wie die Variable i) steht der Platzhalter-Operator für einen beliebigen Wert in einem Array, ein beliebiges Element in einem Set oder einen beliebigen Schlüssel in einem Objekt. Im Grunde ist der Platzhalter eine Variable ohne Namen – eine Wegwerfvariable. In der obigen Regel prüft OPA, ob ein beliebiger Name im input.users-Array ungleich "alice" ist (und fügt ihn gegebenenfalls zum nonadmins-Set hinzu). Das Ergebnis ist genau dasselbe, als hätten Sie name := input.users[i] verwendet.

Wenn Sie den Index dagegen in einer Regel nachverfolgen müssen, sollten Sie eine benannte Variable verwenden, wie unten:

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

Mit dem folgenden Eingabedokument …

{
  "users": [
    "alice",
    "bob",
    "carlotta"
  ]
}

… würden Sie die folgende Ausgabe erhalten:

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

In diesem Fall müssen wir eine benannte Variable verwenden, weil der Name, den wir in der Abfrage input.users[i] != "alice" prüfen, mit dem Namen übereinstimmen soll, den wir in der Abfrage msg ausgeben. Deshalb müssen wir den Index nachverfolgen. Das lässt sich leichter nachvollziehen, wenn Sie sich ansehen, was OPA im Hintergrund tut: Es ersetzt die Variable i durch einen Index. Hier ein Beispiel für einen Durchlauf durch das input.users-Array:

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

Wir müssen in beiden Abfragen input.users[2] verwenden, um sicherzustellen, dass wir auf den Wert am selben Index verweisen (in diesem Fall "carlotta").

Objekt-Comprehension

Eine Objekt-Comprehension fügt einer Variablen nacheinander Elemente hinzu und erzeugt so ein Objekt – ähnlich wie Set-Regeln Sets erzeugen. Während eine Set-Regel jedoch eine Sammlung eindeutiger Werte erstellt, soll eine Objektregel eine Sammlung von Schlüssel-Wert-Paaren erzeugen.

Das Vorgehen ähnelt dem Schreiben einer Set-Regel. Bei einer Objektregel geben Sie jedoch im Regelkopf den Wert des Schlüssel-Wert-Paars an:

nonadmins[name] := "logged-in" {
  name = input.users[i]
  name != "alice"
}

Dieses Beispiel ist genau wie unser erstes Beispiel für eine Set-Regel, mit dem Unterschied, dass hier der Wert jedes Schlüssel-Wert-Paars als "logged-in" festgelegt wird.

Verwenden wir dasselbe Eingabedokument:

{
  "users": [
    "alice",
    "bob",
    "carlotta"
  ]
}

Wenn wir die Regel anhand der obigen Eingabe auswerten, erhalten wir folgende Ausgabe:

{
  "nonadmins": {
    "bob": "logged-in",
    "carlotta": "logged-in"
  }
}

Das Ergebnis ist ein nonadmins-Objekt mit zwei Schlüssel-Wert-Paaren. Jedes Paar hat einen Benutzernamen als Schlüssel und "logged-in" als Wert.

Funktionen

Funktionen in Rego ähneln Funktionen in anderen Sprachen: Sie bieten eine modulare, wiederverwendbare Möglichkeit, dem Programm mitzuteilen, dass es etwas tun soll. Die Syntax von Funktionen ähnelt der von Regeln. Beide definieren Abfragen auf dieselbe Weise, aber eine Funktion enthält einen Parameter (der als Platzhalter für ein tatsächliches Argument dient) in Klammern.

Die folgende Funktion nimmt beispielsweise den Wert von x, verdoppelt ihn und weist das Ergebnis y zu.

double_function(x) := y {
  y := x + x
}

An anderer Stelle im Paket können Sie die Funktion aufrufen, indem Sie ihr ein Argument übergeben, auf das sie angewendet werden soll:

z := double_function(2)

Und z würde den Wert 4 ergeben.

Sie können die Funktion erneut aufrufen, ihr das Argument 12 übergeben und das Ergebnis foo zuweisen. foo hätte dann den Wert 24:

foo := double_function(12)

Sie können eine Funktion verwenden, wenn Sie eine ganz bestimmte Aufgabe mehrmals ausführen müssen, insbesondere innerhalb anderer Funktionen. Das ist hilfreich, wenn Sie übersichtlicheren, modulareren Code schreiben möchten. Wenn Sie eine Aufgabe mit verschiedenen Eingaben wiederholt ausführen, können Sie dafür eine Funktion schreiben.

Sie können auch „Hilfsfunktionen“ schreiben, die in anderen Funktionen verwendet werden. Unten wird der Variablen allow der Wert true zugewiesen, wenn alle Elemente im Array input.tags gültig sind. Um zu bestimmen, ob ein Element gültig ist, prüfen die Hilfsfunktionen is_lowercase_value und is_long_enough, ob ein Zeichenfolgenargument nur Kleinbuchstaben enthält beziehungsweise die richtige Länge hat. Beide werden in der Funktion is_valid verwendet, die von allow aufgerufen wird:

is_lowercase_value(tag) {
  lower(tag) == tag
}

is_long_enough(tag) {
  count(tag) >= 3
}

is_valid(tag) {
  is_lowercase_value(tag)
  is_long_enough(tag)
}

allow {
  tag := input.tags[i]
  is_valid(tag)
}

Beispielregeln mit OPA auswerten

Experimentieren wir mit Set-Regeln, Objektregeln und Funktionen in Rego. Wie in den vorherigen Blogbeiträgen konzentrieren wir uns auf zwei Möglichkeiten, mit OPA zu interagieren:

  • OPA Playground verwenden

  • Das OPA-Befehlszeilentool verwenden

Anweisungen zur Verwendung dieser Schnittstellen finden Sie in Teil 1.

Auch diesmal verwenden wir ein realitätsnäheres Beispiel mit einem Kubernetes-Pod. Hier ist das JSON-Manifest, das wir als Eingabe verwenden:

{
  "apiVersion": "v1",
  "kind": "Pod",
  "metadata": {
    "name": "mypod",
    "labels": {
      "stage": "prod"
    }
  },
  "spec": {
    "shareProcessNamespace": true,
    "containers": [
      {
        "name": "myapp1",
        "image": "myapp1:latest"
      },
      {
        "name": "myapp2",
        "image": "myapp2"
      }
    ]
  }
}

Und hier ist die Richtlinie, anhand derer wir es auswerten und die Unternehmensvorgabe durchsetzen: „Container in Pods der Produktionsumgebung dürfen nicht das neueste Image verwenden“:

is_labeled_prod(labels) {
  labels.stage == "prod"
} {
  labels.stage == "production"
}

latest_containers[container] {
  container := input.spec.containers[_]
  endswith(container.image, ":latest")
}

deny[msg] {
  input.kind == "Pod"
  is_labeled_prod(input.metadata.labels)
  container = latest_containers[_]
  msg := sprintf("Container %v is using latest image on prod", [container.name])
}

Diese Regeln veranschaulichen mehrere Konzepte, die wir in diesem Blogbeitrag besprochen haben, etwa Funktionen, Set-Regeln, Iteration und den Unterstrich-Operator. So funktioniert das Ganze:

is_labeled_prod(labels) — Das ist eine Funktion, die true zurückgibt, wenn die übergebene Menge von Labels ein Label "stage" mit dem Wert "prod" oder "production" enthält.

latest_containers[container] — Das ist eine Set-Regel. OPA verwendet den Unterstrich-Operator als Iterator, um zu prüfen, ob einer der Container in der Eingabe das neueste Image verwendet. Ist das der Fall, wird er der Menge latest_containers hinzugefügt. Ob ein Image das neueste ist, ermitteln wir, indem wir prüfen, ob der Image-Name mit ":latest" endet.

deny[msg] — Auch das ist eine Set-Regel! Sie ist eine fortgeschrittenere Version der Set-Regel deny[msg], die wir in Teil 2 gezeigt haben. Sie umfasst drei Abfragen, für die OPA folgende Aktionen ausführt:

  1. Prüft, ob input.kind den Wert „Pod“ hat.

  2. Ruft die Funktion is_labeled_prod auf und übergibt ihr input.metadata.labels, um zu prüfen, ob Labels "stage" mit dem Wert "prod" oder "production" vorhanden sind.

  3. Durchläuft die Menge latest_containers, um zu prüfen, ob Container darin enthalten sind.

WENN die drei obigen Abfragen true ergeben (d. h. OPA für jede Bedingung eine Übereinstimmung in der Eingabe findet), DANN fügt OPA der Menge deny eine benutzerdefinierte Meldung mit dem Namen des nicht konformen Containers hinzu.

Zu Ihrer Bequemlichkeit haben wir ein Playground-Beispiel mit diesen Inhalten erstellt: https://play.openpolicyagent.org/p/HgHE4w2b4y 

Wenn Sie die Regeln auswerten, indem Sie im Playground auf die Schaltfläche Evaluate klicken oder bei lokaler Ausführung von OPA einen Befehl wie opa eval -i input.json -d check_prod_pod.rego "data.rules.check_prod_pod" --format pretty ausführen, sehen Sie folgende Ausgabe:

{
  "deny": [
    "Container myapp1 is using latest image on prod"
  ],
  "latest_containers": [
    {
      "image": "myapp1:latest",
      "name": "myapp1"
    }
  ]
}

Das erste Element der Ausgabe ist die Menge deny. Sie enthält eine Meldung, dass der Container myapp1 nicht unserer Richtlinie entspricht. Außerdem sehen Sie die Elemente in der Menge latest_containers, die den Namen und das Image jedes Containers enthält – in diesem Fall ist nur der Container myapp1 enthalten.

Sehen wir uns an, was passiert, wenn wir den Image-Namen für myapp2 in myapp2:latest ändern (Zeile 19). Wenn wir die Regeln erneut auswerten, enthält die Menge deny nun Meldungen für myapp1 und myapp2, und die Menge latest_containers enthält beide Container:

{
  "deny": [
    "Container myapp1 is using latest image on prod",
    "Container myapp2 is using latest image on prod"
  ],
  "latest_containers": [
    {
      "image": "myapp1:latest",
      "name": "myapp1"
    },
    {
      "image": "myapp2:latest",
      "name": "myapp2"
    }
  ]
}

Entfernen wir schließlich die Zeile "stage": "prod" (Zeile 7), sodass die Eingabe wie folgt aussieht:

    "labels": {
    }

Wenn wir die Regeln jetzt auswerten, sehen wir, dass latest_containers weiterhin beide Container enthält, die Menge deny jedoch leer ist:

{
  "deny": [],
  "latest_containers": [
    {
      "image": "myapp1:latest",
      "name": "myapp1"
    },
    {
      "image": "myapp2:latest",
      "name": "myapp2"
    }
  ]
}

Da der Pod nicht als Produktionsumgebung gekennzeichnet ist, dürfen seine Container gemäß der Unternehmensrichtlinie das neueste Image verwenden. Der Pod entspricht also der Richtlinie!

Wie geht es weiter?

Herzlichen Glückwunsch! Sie haben unsere dreiteilige Blogreihe zum Schreiben von Rego abgeschlossen.

Wenn Sie mehr erfahren möchten, finden Sie hier einige hilfreiche Ressourcen:

Wenn Sie Rego zum Schreiben benutzerdefinierter Regeln für Snyk IaC verwenden möchten, finden Sie hier unsere Dokumentation. Zusätzlich zu den integrierten Sicherheits- und Compliance-Regelsets von Snyk ermöglichen Ihnen benutzerdefinierte IaC+-Regeln, individuelle Sicherheitskontrollen über Ihren gesamten SDLC hinweg festzulegen.

IaC+ bietet Ihnen eine zentrale Übersicht und Kontrollmöglichkeiten für Ihre Konfigurationsprobleme – vom Code bis zur Cloud. Dazu gehören eine Issues-Benutzeroberfläche, ein Regelset und eine Richtlinien-Engine für IDE, SCM, CLI, CI/CD, Terraform Cloud sowie bereitgestellte Cloud-Umgebungen wie AWS, Azure und Google Cloud.

Starten Sie mit Capture the Flag

Erfahren Sie in unserem virtuellen On-Demand-Workshop für Einsteiger, wie Sie Capture-the-Flag-Herausforderungen lösen.