Skip to content

SARIF output uses generic rule IDs (e.g. main/deny) instead of specific rule names (main/deny_add_usage`), losing rule-level specificity #1396

Description

@jfanjoy

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:

  1. 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.
  2. 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).
  3. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions