SARIF report
--sarif <path> writes a SARIF 2.1.0 log — the standard format GitHub Code Scanning (and other SARIF-consuming dashboards) expect — instead of, or alongside, --json's raw engine result.
$ surea11y scan ./dist/index.html --sarif results.sarif
See CI integration for a ready-to-paste GitHub Actions workflow that runs a scan and uploads results.sarif to the "Security" tab.
Why a separate format from --json
--json's raw result (see Output schema) is this engine's own contract, versioned and stable per API_STABILITY.md. SARIF is a different, externally-defined contract purpose-built for code-scanning dashboards — a checksResults[] entry and a SARIF result don't map 1:1, so this is a real conversion, not a re-serialization.
What becomes a SARIF result
Only fail/cantTell occurrences produce SARIF results (same "violations only" framing as the HTML report).
A notApplicable check is not always empty: a rule may attach one occurrence explaining why it had nothing to judge, which the contrast rules do when no text had a computable background. Those never become results — a consumer treats every result as an alert, and "this was not evaluated" is not one — but they are not dropped either. They are carried as note-level entries in runs[0].invocations[0].toolExecutionNotices, each naming the rule it came from via associatedRule.id:
"invocations": [{"executionSuccessful": true,"toolExecutionNotices": [{"level": "note","message": { "text": "No eligible text had computable contrast (eligible text nodes: 13). See the contrast computability rule for details." },"associatedRule": { "id": "contrast-minimum" }}]}]
That keeps a SARIF-only pipeline from reading silence as a clean bill of health: no contrast alerts can mean the page is fine, or that contrast was never computable, and only the notice separates the two. The block is emitted only when there is something to say, so a run with nothing to report has no invocations key at all.
| Engine outcome | SARIF level | Meaning |
|---|---|---|
Every rule that ran (regardless of whether it produced a result) is listed once in runs[0].tool.driver.rules, with defaultConfiguration.level set from the rule's type: automatic (fail-capable) → error, manual (capped at cantTell) → warning.
Field mapping
| SARIF field | Source |
|---|---|
Locations
DOM-based scanning has no line/column to report, so physicalLocation.artifactLocation.uri is the scanned target itself, not a source-file position:
- Local file scans: a path relative to the current working directory (forward-slashed). If this matches a real file in your repository, GitHub Code Scanning can render the finding as an inline annotation.
- URL scans: the scanned URL itself. GitHub Code Scanning will still list the finding, but can't attach an inline annotation to a URL that isn't a file in the repository — this is inherent to how SARIF/Code Scanning associate findings with source, not a surea11y limitation. If you need inline annotations, scan the rendered HTML file (e.g. a build output artifact) rather than a live URL.
occurrence.selector is additionally carried as a logicalLocations[].fullyQualifiedName, so a consumer that reads logical locations still gets the "which element" signal even without a usable physical location.
Combining with --baseline
A generic SARIF consumer has no "known, don't gate on this" concept of its own — the only faithful way to honor a baseline in SARIF output is to omit already-known fail occurrences entirely, rather than downgrade them to warning:
$ surea11y scan ./dist/index.html --baseline baseline.json --sarif results.sarif
cantTell occurrences are never filtered by a baseline — the baseline mechanism only ever tracks fail occurrences (matching --write-baseline, see Baseline / allowlist).
Combining with --html/--json
--sarif, --html, and --json are independent output flags — pass any combination in one run; each writes/prints its own report from the same single scan.