When conftest produces SARIF output, all deny rules share ruleId: "main/deny" and all warn rules share ruleId: "main/warn". The specific rule name (e.g., deny_add_usage, warn_base_image_host) is available — it's placed in properties.query as data.main.deny_add_usage — but only for rules that happen to appear in the rules[] array. Rules that fire but aren't queried individually don't appear in rules[] at all.
Steps to reproduce
Given a policy with multiple deny rules:
package main
deny_add_usage contains "Use COPY instead of ADD" if {
some i
input[i].Cmd == "add"
}
deny_curl_wget contains sprintf("Line %d: curl/wget detected", [i]) if {
some i
input[i].Cmd == "run"
regex.match(`.*\b(curl|wget)\b.*://.*`, input[i].Value[0])
}
Run:
conftest test --output sarif --namespace main Dockerfile
Current behavior
The SARIF output produces:
{
"runs": [{
"tool": {
"driver": {
"rules": [
{ "id": "main/deny", "properties": { "query": "data.main.deny_add_usage" } },
{ "id": "main/warn", "properties": { "query": "data.main.warn_base_image_host" } }
]
}
},
"results": [
{ "ruleId": "main/deny", "ruleIndex": 0, "message": { "text": "Use COPY instead of ADD" } },
{ "ruleId": "main/deny", "ruleIndex": 0, "message": { "text": "Line 2: curl/wget detected" } }
]
}]
}
Problems:
- Both deny results have the same ruleId (main/deny) and the same ruleIndex (0). There is no way to distinguish deny_add_usage from deny_curl_wget in the results.
- The rules[] array is incomplete — only rules from conftest's initial discovery pass are listed. If deny_curl_wget fires but wasn't in the discovery set, its properties.query never appears in rules[] at all. The ruleIndex for that result points to whichever rule happened to be at that index (often a different rule entirely).
- SARIF consumers can't group or filter by specific rule — VSCode's SARIF Viewer, GitHub code scanning, GitLab vulnerability reports, and Azure DevOps all use ruleId for deduplication, trend tracking, and suppression. All deny violations collapse into a single "main/deny" bucket.
Expected behavior
Each named Rego rule should produce a unique ruleId derived from its rule name:
{
"ruleId": "main/deny_add_usage",
"ruleIndex": 0,
...
}
The specific rule name is already available — properties.query contains data.main.deny_add_usage. Converting data.main.deny_add_usage to main/deny_add_usage (strip data. prefix, swap . for /) would give each rule a unique, stable, meaningful ruleId.
This would make rules[] complete (one entry per actual rule that fired), ruleIndex would be a reliable back-reference, and SARIF consumers could properly group, filter, suppress, and trend individual rules.
Root cause
In output/sarif.go, getRuleID constructs the rule ID from namespace and a generic rule type:
func getRuleID(namespace string, ruleType string) string {
return fmt.Sprintf("%s/%s", namespace, ruleType)
}
And ruleType is always one of "deny", "warn", "allow", "success", or "skip" — hardcoded in the Output method. The actual rule name from Result.Metadata["query"] is placed in properties.query but never used for ruleId. To mitigate this, it could be:
- Gated behind a flag (e.g., --sarif-rule-id=specific|generic, defaulting to generic for backward compatibility)
- Released as opt-in with a migration period
- Changed in the next major version
Environment
conftest version: 0.68.2 (current master)
SARIF output format
Impact
This affects any SARIF consumer that needs to distinguish between individual policy rules: vulnerability management platforms, IDE integrations, compliance dashboards, and trend analysis tools. The current output makes it impossible to track which specific policy triggered a result across runs, since all deny rules are indistinguishable.
When conftest produces SARIF output, all deny rules share ruleId: "main/deny" and all warn rules share ruleId: "main/warn". The specific rule name (e.g., deny_add_usage, warn_base_image_host) is available — it's placed in properties.query as data.main.deny_add_usage — but only for rules that happen to appear in the rules[] array. Rules that fire but aren't queried individually don't appear in rules[] at all.
Steps to reproduce
Given a policy with multiple deny rules:
Run:
conftest test --output sarif --namespace main DockerfileCurrent behavior
The SARIF output produces:
{ "runs": [{ "tool": { "driver": { "rules": [ { "id": "main/deny", "properties": { "query": "data.main.deny_add_usage" } }, { "id": "main/warn", "properties": { "query": "data.main.warn_base_image_host" } } ] } }, "results": [ { "ruleId": "main/deny", "ruleIndex": 0, "message": { "text": "Use COPY instead of ADD" } }, { "ruleId": "main/deny", "ruleIndex": 0, "message": { "text": "Line 2: curl/wget detected" } } ] }] }Problems:
Expected behavior
Each named Rego rule should produce a unique ruleId derived from its rule name:
{ "ruleId": "main/deny_add_usage", "ruleIndex": 0, ... }The specific rule name is already available — properties.query contains data.main.deny_add_usage. Converting data.main.deny_add_usage to main/deny_add_usage (strip data. prefix, swap . for /) would give each rule a unique, stable, meaningful ruleId.
This would make rules[] complete (one entry per actual rule that fired), ruleIndex would be a reliable back-reference, and SARIF consumers could properly group, filter, suppress, and trend individual rules.
Root cause
In output/sarif.go, getRuleID constructs the rule ID from namespace and a generic rule type:
And ruleType is always one of "deny", "warn", "allow", "success", or "skip" — hardcoded in the Output method. The actual rule name from Result.Metadata["query"] is placed in properties.query but never used for ruleId. To mitigate this, it could be:
Environment
conftest version: 0.68.2 (current master)
SARIF output format
Impact
This affects any SARIF consumer that needs to distinguish between individual policy rules: vulnerability management platforms, IDE integrations, compliance dashboards, and trend analysis tools. The current output makes it impossible to track which specific policy triggered a result across runs, since all deny rules are indistinguishable.