Automate Triage with OPA¶
This guide walks you through building a policy set that automates 80%+ of your triage decisions, turning a manual bottleneck into a fast, consistent, policy-driven process.
Goal¶
By the end of this guide, you will have:
- A set of triage policies covering common patterns
- An automation rate above 80%
- A clean triage queue with only findings that genuinely need human review
Time: ~25 minutes
Prerequisites¶
- At least one completed scan with 50+ findings
- Familiarity with the Policy Playground
- Admin or policy-editor role
Step 1 — Assess your current state¶
- Navigate to the Triage view.
- Note the total finding count and breakdown:
- How many are Critical? High? Medium? Low? Info?
- How many are from each scanner?
- How many are in test/vendor directories?
Write down the numbers — you'll compare after automation.
Step 2 — Create the severity router¶
This foundational policy handles the most common triage pattern — routing by severity:
- Navigate to Policies > New Policy.
- Kind: Triage, Name:
triage-severity-routing. - Paste:
package mayo.triage
import rego.v1
default decision := "defer"
# Auto-accept critical and high findings
decision := "accept" if {
input.finding.severity in ["critical", "high"]
}
# Auto-reject informational findings
decision := "reject" if {
input.finding.severity == "info"
}
reason := "Auto-accepted: critical or high severity" if {
decision == "accept"
}
reason := "Auto-rejected: informational severity" if {
decision == "reject"
}
- Test in the playground with sample findings of each severity.
- Save & Activate.
Step 3 — Create the path filter¶
Suppress findings in non-production code:
- Create a new policy:
triage-non-production-paths. - Paste:
package mayo.triage
import rego.v1
non_production_patterns := [
"/test/",
"/__tests__/",
"/spec/",
"/fixtures/",
"/testdata/",
"/vendor/",
"/node_modules/",
"/.github/",
"/docs/",
"/examples/"
]
decision := "reject" if {
some pattern in non_production_patterns
contains(input.finding.file_path, pattern)
}
reason := "Auto-rejected: finding is in a non-production path" if {
decision == "reject"
}
- Test with findings from test directories.
- Save & Activate.
Step 4 — Create the noise filter¶
Suppress known-noisy scanner rules:
- Create a new policy:
triage-noisy-rules. - Start with an empty blocklist and populate it from your triage queue:
package mayo.triage
import rego.v1
# Add rules here as you identify noise from your scans
noisy_rules := {
"generic.secrets.gitleaks.generic-api-key",
"generic.secrets.gitleaks.generic-secret"
}
decision := "reject" if {
input.finding.rule_id in noisy_rules
}
reason := concat("", [
"Auto-rejected: noisy rule ",
input.finding.rule_id
]) if {
decision == "reject"
}
- Save & Activate.
Populating the blocklist
Review your triage queue over the next few days. Each time you manually reject a finding for the same rule, add that rule_id to the noisy_rules set and update the policy.
Step 5 — Create the KEV/EPSS booster¶
Auto-accept findings that are actively exploited:
- Create a new policy:
triage-threat-intel. - Paste:
package mayo.triage
import rego.v1
# Auto-accept CISA KEV findings regardless of severity
decision := "accept" if {
input.finding.in_kev == true
}
# Auto-accept high-EPSS findings
decision := "accept" if {
input.finding.epss_score > 0.5
input.finding.severity in ["critical", "high", "medium"]
}
reason := "Auto-accepted: in CISA KEV catalog" if {
input.finding.in_kev == true
}
reason := "Auto-accepted: high EPSS score" if {
input.finding.epss_score > 0.5
}
- Save & Activate.
Step 6 — Re-evaluate all findings¶
Apply your new policies to existing findings:
- Navigate to Findings.
- Click Re-evaluate Triage.
- Wait for evaluation to complete (typically 10-30 seconds for a few hundred findings).
Step 7 — Check your automation rate¶
- Navigate to the Triage Funnel Dashboard.
- Compare before and after:
| Metric | Before | After (expected) |
|---|---|---|
| Auto-accepted | 0% | 20-30% |
| Auto-rejected | 0% | 40-60% |
| Deferred/Unmatched | 100% | 15-30% |
| Automation rate | 0% | 70-85% |
80% or higher?
If you hit 80%, congratulations. If not, review the deferred queue for additional patterns to automate.
Step 8 — Iterate on the deferred queue¶
The remaining deferred findings need attention:
- Open the Triage Queue (shows only deferred/unmatched findings).
- For each finding, make a decision:
- Accept — it's a real issue
- Reject — it's noise
- Create policy — if you see a pattern
- Repeat weekly until the queue is manageable (< 20 findings per week).
Final policy set summary¶
| Policy | Purpose | Expected impact |
|---|---|---|
triage-severity-routing |
Route by severity | 30-40% of findings |
triage-non-production-paths |
Suppress test/vendor | 15-25% of findings |
triage-noisy-rules |
Suppress known noise | 10-20% of findings |
triage-threat-intel |
Boost KEV/EPSS findings | 2-5% of findings |
Verification¶
- All four policies are active
- Findings have been re-evaluated
- Automation rate is > 80%
- Triage queue contains only genuinely uncertain findings
Next steps¶
- Generate Jira tickets — create tickets from accepted findings
- Priority policies — score accepted findings
- Triage workflows — ongoing maintenance