Skip to content

Writing Rego for Mayo ASPM

This guide covers Rego v1 syntax as it applies to Mayo ASPM policies. It assumes no prior Rego experience and builds from basic concepts to advanced patterns.


Rego basics

Rego is a declarative query language designed for policy. You write rules that evaluate to true or produce a value when their conditions are met.

Package declaration

Every policy must start with a package declaration matching its kind:

package mayo.triage      # for triage policies
package mayo.priority    # for priority policies
package mayo.ownership   # for ownership policies
package mayo.project     # for project policies
package mayo.pr_scan     # for PR scan policies

Import statement

Always include the v1 import:

import rego.v1

This enables if, in, contains, every, and other v1 keywords.


Rules and assignments

Simple assignment

# Assign a string value
decision := "accept" if {
    input.finding.severity == "critical"
}

Default values

# If no rule matches, use this default
default decision := "defer"

decision := "accept" if {
    input.finding.severity == "critical"
}

decision := "reject" if {
    input.finding.severity == "info"
}

Multiple conditions (AND)

All conditions in a rule body must be true (logical AND):

decision := "accept" if {
    input.finding.severity == "critical"    # AND
    input.finding.cve_id != ""              # AND
    input.finding.scanner == "grype"        # all must be true
}

Multiple rules (OR)

Multiple rules with the same head act as logical OR:

# Accept if critical
decision := "accept" if {
    input.finding.severity == "critical"
}

# Also accept if high + has CVE
decision := "accept" if {
    input.finding.severity == "high"
    input.finding.cve_id != ""
}

Working with strings

import rego.v1

# Exact match
input.finding.scanner == "semgrep"

# Contains
contains(input.finding.file_path, "/test/")

# Starts with
startswith(input.finding.rule_id, "java.lang.security")

# Ends with
endswith(input.finding.file_path, ".test.js")

# Regex match
regex.match(`CVE-2026-\d+`, input.finding.cve_id)

# String concatenation
msg := concat(" ", ["Finding", input.finding.id, "is", input.finding.severity])

Working with collections

The in keyword

# Check membership in a list
input.finding.severity in ["critical", "high"]

# Check membership in an object's keys
"lodash" in input.finding.dependencies

Iteration

# Check if any dependency has a critical CVE
some dep in input.finding.dependencies
dep.severity == "critical"

Comprehensions

# Collect all critical dependency names
critical_deps := [dep.name |
    some dep in input.finding.dependencies
    dep.severity == "critical"
]

The input object

Each policy kind receives a different input structure. Here are the top-level schemas:

Triage policy input

{
  "finding": {
    "id": "string",
    "title": "string",
    "severity": "critical|high|medium|low|info",
    "scanner": "string",
    "rule_id": "string",
    "file_path": "string",
    "line_number": 0,
    "cve_id": "string",
    "cwe_id": "string",
    "package": "string",
    "package_version": "string",
    "fixed_version": "string",
    "age_days": 0,
    "asset": { "name": "string", "source": "string", "full_name": "string" },
    "project": { "id": "string", "name": "string" }
  },
  "organization": { "id": "string", "name": "string" }
}

Priority policy input

Same as triage, plus:

{
  "finding": { "...": "same as triage" },
  "triage_decision": "accept",
  "organization": { "...": "..." }
}

Ownership policy input

Same as triage, plus:

{
  "finding": { "...": "same as triage" },
  "teams": [
    { "id": "string", "name": "string", "members": ["string"] }
  ]
}

Project policy input

{
  "asset": {
    "name": "string",
    "source": "github|upload|api",
    "full_name": "string",
    "language": "string",
    "topics": ["string"]
  }
}

PR scan policy input

{
  "pull_request": {
    "number": 0,
    "title": "string",
    "author": "string",
    "base_branch": "string",
    "head_branch": "string"
  },
  "scan_results": {
    "new_findings": [{ "...": "finding object" }],
    "fixed_findings": [{ "...": "finding object" }],
    "total_new": 0,
    "total_fixed": 0,
    "by_severity": {
      "critical": 0,
      "high": 0,
      "medium": 0,
      "low": 0,
      "info": 0
    }
  }
}

Common patterns

Severity-based routing

package mayo.triage

import rego.v1

default decision := "defer"

decision := "accept" if {
    input.finding.severity in ["critical", "high"]
}

decision := "reject" if {
    input.finding.severity == "info"
}

File-path-based filtering

decision := "reject" if {
    some pattern in ["/test/", "/spec/", "/vendor/", "/node_modules/"]
    contains(input.finding.file_path, pattern)
}

Priority scoring

package mayo.priority

import rego.v1

severity_score := 100 if input.finding.severity == "critical"
severity_score := 75 if input.finding.severity == "high"
severity_score := 50 if input.finding.severity == "medium"
severity_score := 25 if input.finding.severity == "low"

exploit_bonus := 20 if {
    input.finding.cve_id != ""
    input.finding.fixed_version != ""
}
exploit_bonus := 0 if not input.finding.fixed_version

priority := severity_score + exploit_bonus

Debugging tips

  1. Use the Playground — test with real finding data from your scans.
  2. Print intermediate values — use print() in the playground for debugging (stripped in production).
  3. Start simple — write one rule, test it, then add complexity.
  4. Check for conflicts — two rules producing different values for the same output cause errors.

Common mistakes

  • Forgetting import rego.v1
  • Using = instead of := for assignment
  • Using == instead of := in rule heads
  • Missing quotes around string values

Next steps