InteroperabilityPaul White11 August 20266 min read

SARIF for pentest findings: piping results into developer workflows

Most findings die in a PDF. SARIF is the language GitHub, Azure DevOps and CI already speak, so findings arrive as alerts in the developer’s own queue instead of an attachment.

Most pentest findings die in a PDF. An engineer reads them once, copies a few into a ticket by hand, and the rest never reach the place where code actually gets fixed. SARIF is the format that closes that gap: it is the language GitHub, Azure DevOps, and a long list of CI tools already speak for security results. Emit your findings as SARIF and they stop being an attachment and start being alerts in the developer’s own workflow.

SARIF (the Static Analysis Results Interchange Format) is an OASIS standard, a JSON shape for “a tool found these problems in this code.” It was designed for static analysers, but nothing about it is specific to them. A human-authored pentest finding with a location, a severity, and a description fits the same shape, and once it is in that shape the developer tooling treats it exactly like any other scan result.

Why bother routing findings through SARIF

The value is not the format. It is where the format is already accepted. Upload SARIF to GitHub and each finding becomes a code-scanning alert on the exact file and line, with the right severity, visible in the security tab and on the pull request that touches that code. Azure DevOps, GitLab, and several IDEs read it too. You are not asking the engineering team to adopt a new tool. You are handing their existing tool a file it already knows how to open.

A finding in a PDF is a request for someone to do data entry. A finding as a code-scanning alert is already in the queue, already assigned to the code, already tracked to closure. The format change is small; the change in whether it gets fixed is not.

What a finding looks like as SARIF

The core of a SARIF file is a run with a tool and a list of results. Each result carries a rule id, a level, a message, and a location. Here is one finding, trimmed to the parts that matter:

one finding, as SARIF 2.1.0

{
  "version": "2.1.0",
  "runs": [{
    "tool": { "driver": { "name": "Cairn", "rules": [
      { "id": "CWE-89", "name": "SQL Injection" }
    ] } },
    "results": [{
      "ruleId": "CWE-89",
      "level": "error",
      "message": { "text": "Blind SQL injection in the report filter parameter." },
      "properties": { "security-severity": "9.8" },
      "locations": [{ "physicalLocation": {
        "artifactLocation": { "uri": "app/reports/filter.py" },
        "region": { "startLine": 42 }
      } }]
    }]
  }]
}

Two details do the heavy lifting. The locations array is what pins the finding to a file and line so the alert lands on the right code. And properties.security-severity is the numeric string (a CVSS-style 0.0 to 10.0) that GitHub ranks on, so a 9.8 shows as critical and a 5.5 as medium. Get those two right and the platform does the rest.

The severity mapping is where findings usually get lost

SARIF’s own level field only has three useful values: error, warning, and note. Pentest findings have five severities, from informational to critical. If you map severity to level alone, a critical and a high both collapse to “error” and the ranking that makes a triage queue useful is gone.

The fix is to carry the real severity in security-severity as a number, and let level be the coarse bucket. A finding scored with a CVSS vector already has that number. When a finding has no CVSS score, derive one from its severity band so it still sorts into the right place instead of defaulting to the bottom of the list. That single mapping decision is the difference between findings that arrive triaged and findings that arrive as an undifferentiated pile.

A finding library is a natural SARIF source

SARIF is a transport format, not a place to author or keep findings. It flattens the structured write-up (separate description, impact, and remediation fields, CVSS as a first-class value) that a good finding carries, because the developer tooling on the far end does not need all of it. So the sensible pattern is to author and keep findings in a structured format and emit SARIF at the boundary.

That is exactly what the Open Pentest Format is for, and why opf-tools converts an OPF finding library to SARIF with one command. It handles the severity mapping above, drops CWE and CVE identifiers into their native SARIF slots, and reads SARIF back into OPF so a scanner’s output can re-enter an editable library. The pipeline is short:

library to alerts, in two steps

# OPF finding library to SARIF, then into GitHub code scanning
npx @cairnsec/opf-tools sarif library.opf.json > findings.sarif.json

# upload with the standard action in CI
- uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: findings.sarif.json

Where the round trip helps

Because opf-tools reads SARIF as well as writing it, the flow goes both ways. A scanner emits SARIF in CI; you pull it into OPF, enrich the findings with real impact and remediation in a proper editor, and push the results back out as SARIF for the developers, or into the report for the client. The developer workflow and the pentest report stop being two disconnected records of the same findings.

Pipe findings where the fixing happens
opf-tools converts an OPF finding library to SARIF (and back), MIT licensed and with no dependencies. Cairn reads and writes OPF from its Finding Library, so the same findings reach the report and the developer’s queue.
Read about the converters
← All posts