Triage Policy Reference¶
Triage policies evaluate incoming findings and assign a triage decision: accept, reject, or defer. They are the first line of defense against alert fatigue.
Package¶
Required output¶
| Variable | Type | Values | Description |
|---|---|---|---|
decision |
string | "accept", "reject", "defer" |
The triage decision |
Optional outputs:
| Variable | Type | Description |
|---|---|---|
reason |
string | Human-readable explanation shown in the finding timeline |
tags |
array of strings | Tags to add to the finding |
Input schema¶
{
"finding": {
"id": "string — unique finding identifier",
"title": "string — human-readable title",
"description": "string — detailed description",
"severity": "string — critical|high|medium|low|info",
"scanner": "string — scanner name (semgrep, grype, trivy, gitleaks, etc.)",
"scanner_type": "string — sast|sca|secret|container|iac",
"rule_id": "string — scanner-specific rule identifier",
"file_path": "string — relative file path within the asset",
"line_number": "integer — line number (0 if not applicable)",
"cve_id": "string — CVE identifier (empty if none)",
"cwe_id": "string — CWE identifier (empty if none)",
"package": "string — affected package name (SCA only)",
"package_version": "string — installed version",
"fixed_version": "string — version that fixes the issue (empty if unknown)",
"age_days": "integer — days since first detected",
"epss_score": "float — EPSS probability (0.0-1.0)",
"in_kev": "boolean — true if in CISA KEV catalog",
"asset": {
"name": "string — asset short name",
"source": "string — github|upload|api",
"full_name": "string — e.g., org/repo"
},
"project": {
"id": "string — project identifier",
"name": "string — project name"
}
},
"organization": {
"id": "string",
"name": "string"
}
}
Examples¶
Basic severity 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"
}
Suppress test and vendor files¶
package mayo.triage
import rego.v1
decision := "reject" if {
some pattern in ["/test/", "/spec/", "/__tests__/", "/vendor/", "/node_modules/"]
contains(input.finding.file_path, pattern)
}
reason := "Finding is in a non-production path" if {
decision == "reject"
}
Accept known-exploited vulnerabilities¶
package mayo.triage
import rego.v1
decision := "accept" if {
input.finding.in_kev == true
}
reason := "CVE is in the CISA Known Exploited Vulnerabilities catalog" if {
input.finding.in_kev == true
}
Scanner-specific suppression¶
package mayo.triage
import rego.v1
# Suppress noisy Semgrep rules
noisy_rules := {
"generic.secrets.gitleaks.generic-api-key",
"python.lang.best-practice.open-never-closed",
"javascript.express.best-practice.helmet"
}
decision := "reject" if {
input.finding.scanner == "semgrep"
input.finding.rule_id in noisy_rules
}
reason := concat("", ["Suppressed noisy rule: ", input.finding.rule_id]) if {
decision == "reject"
}
EPSS-based triage¶
package mayo.triage
import rego.v1
decision := "accept" if {
input.finding.epss_score > 0.5
}
decision := "reject" if {
input.finding.severity == "low"
input.finding.epss_score < 0.01
input.finding.age_days > 180
}
Evaluation behavior¶
- Triage policies run once per finding when the finding is first ingested.
- If the finding is re-detected in a subsequent scan, triage is not re-evaluated unless the finding's attributes change (e.g., severity updated by the scanner).
- Multiple triage policies can exist. If multiple policies produce different decisions for the same finding, the most specific scoped policy wins. See Policy scoping.
Testing tips¶
- Load a real finding from your scan data into the playground.
- Verify that the decision matches your expectations.
- Test edge cases: empty
cve_id, missingfile_path, zeroage_days. - Save test cases for regression testing.
Next steps¶
- Writing Rego — Rego syntax reference
- Triage funnel — how triage feeds into ticketing
- Priority policies — assign priority scores after triage