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:
This enables if, in, contains, every, and other v1 keywords.
Rules and assignments¶
Simple assignment¶
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¶
- Use the Playground — test with real finding data from your scans.
- Print intermediate values — use
print()in the playground for debugging (stripped in production). - Start simple — write one rule, test it, then add complexity.
- 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