SARIF-Bericht
--sarif <path> schreibt ein Log im Format SARIF 2.1.0, dem Standardformat, das GitHub Code Scanning (und andere Dashboards, die SARIF lesen) erwartet, statt oder zusätzlich zum rohen Engine-Ergebnis von --json.
$ surea11y scan ./dist/index.html --sarif results.sarif
Unter CI-Integration finden Sie einen GitHub-Actions-Workflow zum direkten Einfügen, der einen Scan ausführt und results.sarif in den Tab „Security“ hochlädt.
Warum ein eigenes Format neben --json
Das rohe Ergebnis von --json (siehe Ausgabeschema) ist der eigene Vertrag dieser Engine, versioniert und stabil gemäß API_STABILITY.md. SARIF ist ein anderer, extern definierter Vertrag, eigens für Code-Scanning-Dashboards gebaut. Ein Eintrag in checksResults[] und ein SARIF-result entsprechen sich nicht 1:1, deshalb ist das eine echte Umwandlung, keine erneute Serialisierung.
Was zu einem SARIF-Ergebnis wird
Nur fail- und cantTell-Vorkommen erzeugen SARIF-Ergebnisse (derselbe Ansatz „nur Verstöße“ wie beim HTML-Bericht). Ein Log ohne Ergebnisse sagt also, dass unter den gelaufenen Prüfungen nichts fail ergab oder eine Prüfung braucht; es sagt nicht, was geprüft wurde, und es ist keine Konformitätsaussage. Dafür bewahren Sie das JSON-Ergebnis auf.
Eine notApplicable-Prüfung ist nicht immer leer: Eine Regel kann ein Vorkommen anhängen, das erklärt, warum sie nichts zu beurteilen hatte; die Kontrastregeln tun das, wenn kein Text einen berechenbaren Hintergrund hatte. Diese Vorkommen werden nie zu Ergebnissen (ein Konsument behandelt jedes Ergebnis als Alarm, und „wurde nicht ausgewertet“ ist keiner), aber sie werden auch nicht verworfen. Sie stehen als Einträge der Stufe note in runs[0].invocations[0].toolExecutionNotifications, jeweils mit der Regel, aus der sie stammen, in associatedRule.id:
"invocations": [{"executionSuccessful": true,"toolExecutionNotifications": [{"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" }}]}]
So liest eine Pipeline, die nur SARIF auswertet, Stille nicht als Unbedenklichkeitsbescheinigung: Keine Kontrast-Alarme können bedeuten, dass die Seite in Ordnung ist, oder dass der Kontrast nie berechenbar war, und nur der Hinweis unterscheidet die beiden Fälle. Der Block wird nur ausgegeben, wenn es etwas zu sagen gibt; ein Lauf ohne Meldungen hat also gar keinen invocations-Schlüssel. Eine Regel, die bestanden hat, schreibt keinen Hinweis.
Eine Regel, die nicht abgeschlossen wurde (sie hat eine Ausnahme geworfen: cantTell ohne Vorkommen und mit einem error), hat nichts beurteilt und ist daher ebenfalls kein Ergebnis. Sie ist ein Hinweis der Stufe error in derselben Liste, "The rule <id> did not complete: <error>", mit der Regel in associatedRule.id, sodass die Lücke in der Abdeckung nicht wie ein Bestehen aussieht.
| Engine-Ergebnis | SARIF-Stufe | Bedeutung |
|---|---|---|
fail | error | Deterministischer Verstoß: der Fall, an dem die CI scheitern soll. |
cantTell | warning | Erfordert manuelle Prüfung: wird angezeigt, sollte aber allein keinen Build blockieren. |
cantTell mit Tendenz zu fail, mit leanFailAs: 'error' | error | Offen, aber die Anhaltspunkte der Seite weisen auf einen Fehler (lean); zählt für eine Schranke auf error. |
Ein offener Befund mit Tendenz führt sie in properties.lean: { "toward": "fail", "why": "The heading skips a level after the one before it." }, unabhängig von der Stufe, mit der er gemeldet wird. Wo die Tendenz sie hat, führt er auch caution, was sie falsch machen könnte ("A layout table can also hold numbers."), und measured, wie oft sie richtig lag ({ "agreed": 47, "cases": 50, "corpus": "…", "coreVersion": "…" }, sobald eine Tendenz an fünf Fällen gezählt wurde, was bisher bei keiner der Fall ist), sodass ein Alert zeigt, was der HTML-Bericht neben der Tendenz zeigt. Ihre signals und i18n-Schlüssel bleiben im Scan-Ergebnis.
Eine Regel, deren sämtliche WCAG-Kriterien außerhalb des Ziels des Scans liegen (eine AAA-Regel unter engineOptions.wcagLevel: 'AA' oder duplicate-id unter WCAG 2.2: die Kriterien, die der Scan aus rulesResults ausgelassen hat, wie bei den wcag-<SC>-Tags unten), ist trotzdem gelaufen, und ihre fail- und cantTell-Befunde sind weiterhin Ergebnisse, auf der Stufe note: So sind sie sichtbar, ohne einen auf das Ziel begrenzten Build scheitern zu lassen. Jedes nennt den Grund in properties.outsideTarget: { "criteria": ["1.4.6"], "target": "WCAG 2.2 AA" }.
Jede ausgeführte Regel (unabhängig davon, ob sie ein Ergebnis erzeugt hat) wird einmal in runs[0].tool.driver.rules aufgeführt, wobei defaultConfiguration.level aus dem type der Regel abgeleitet wird: automatic (kann fail melden) → error, manual (nie fail) → warning. Eine Regel-ID, die in einem gespeicherten Ergebnis kein String ist, wird als ihr Text geschrieben; eine Prüfung ohne Regel-ID nennt keine Regel und wird weggelassen, wie JUnit und EARL sie weglassen.
Feldzuordnung
| SARIF-Feld | Quelle |
|---|---|
results[].ruleId | checksResults[i].ruleId |
results[].message.text | occurrence.summary + occurrence.hint. Ein Vorkommen ohne Zusammenfassung (etwa das einer eigenen Regel) verwendet den Titel der Regel oder ihre ID, da GitHub ein Ergebnis mit leerem Text ablehnt. Bei einem offenen Befund folgen nach einer Leerzeile seine Schritte: How to check:, die nummerierten Schritte, dann Passes when: und Fails when:, sodass ein Alert sagt, wie er sich beantworten lässt. |
results[].properties.howToCheck | Die Schritte eines offenen Befunds, { steps, passWhen, failWhen }, wie occurrence.howToCheck sie liefert. Ein Ergebnis, das ohne sie kommt, wie das eines Bindings, erhält sie in Node; packs (die Packs, die der Scan ausgeführt hat) fügt die eigenen einer Pack-Regel hinzu. Wie die Tendenz nicht Teil der Identität des Befunds. |
results[].locations[].physicalLocation.artifactLocation.uri | Das gescannte Ziel; siehe Fundorte weiter unten. |
results[].locations[].logicalLocations[].fullyQualifiedName | Der Fundort des Elements, mit kind: "element", wenn occurrence.selector ein nicht leerer String ist. Außerhalb eines Shadow Trees ist es der Selektor. Bei einem Element in einem Shadow Tree sind es die Selektoren der Shadow Hosts und dann der eigene des Elements, verbunden mit >>> : html > body > main > x-card >>> button. Die selector:-Zeile von JUnit und der HTML-Bericht zeigen denselben Text. |
results[].partialFingerprints["surea11y/violation/v2"] | Der SHA-256-Digest, 64 Hexadezimalziffern, desselben Identitätsschlüssels ruleId + reasonCode + html, den Baseline / Allowlist (computeBaselineKey) verwendet: ein stabiler, inhaltsbasierter Fingerprint statt eines positionsbasierten, für Konsumenten, die ihn auslesen. Derselbe Befund erhält in jedem Bericht denselben Digest. Vor 1.11 enthielt surea11y/violation/v1 den Schlüssel selbst, daher sieht ein Tool, das v1 gelesen hat, jeden Befund einmalig als neu. Ein Befund, dessen Reason-Code sich ändert, erhält einen anderen Digest: in 1.14 die Befunde der siebzehn Regeln, die jetzt einen Code angeben, und die der Codes, die nach Ursache aufgeteilt wurden; ihre Code-Scanning-Alerts werden also einmal geschlossen, und die Befunde kommen als neu zurück (siehe Baseline / Allowlist). |
results[].partialFingerprints.primaryLocationLineHash | Die ersten 16 Hexadezimalziffern des Digests und eine Zählung, <16 hex digits>:<n> (unverändert gegenüber der Zeit vor v2), wobei n einen auf der Seite wiederholten Befund zählt (dieselbe fehlerhafte Komponente zweimal). GitHub Code Scanning ordnet Alerts zwischen Uploads allein über dieses Feld zu. Ein DOM-Befund hat keine Zeile, die die Upload-Action von GitHub hashen könnte; ohne das Feld wurden Alerts daher nach Position zugeordnet, und das Beheben eines Befunds konnte den Alert eines anderen schließen. Wer von core 1.8 oder älter kommt, sieht beim ersten Upload einmalig, dass die bestehenden Alerts geschlossen und als neue wieder geöffnet werden; ein in GitHub verworfener Alert muss erneut verworfen werden. |
results[].properties.severity / .confidence / .reasonCode / .html | severity/confidence des Ergebnisses selbst, dazu derselbe reasonCode und dasselbe html-Snippet, auf denen der Fingerprint oben beruht. Rein informativ, nicht Teil des SARIF-Schemas selbst. |
tool.driver.rules[].properties.tags | accessibility, automatic/manual und ein wcag-<SC>-Tagpro WCAG-Erfolgskriterium in meta.normativeMappings, das WCAG-Stufe und -Version des Scans behalten. Ein Kriterium, das der Scan ihretwegen aus rulesResults ausgelassen hat, erhält kein Tag: ein AAA-Kriterium unter engineOptions.wcagLevel: 'AA' oder 4.1.1, wenn includeRuleIds: ['duplicate-id'] unter WCAG 2.2 lief. Ein Kriterium, das der Scan meldet, etwa eine per ID genannte Zusammenfassung oberhalb der Stufe, behält sein Tag, ebenso jedes Kriterium eines Ergebnisses, das weder engine.wcagLevel noch engine.wcagVersion festhält. Die Regel bleibt, und ihre Ergebnisse auch, als Notes, wenn jedes Kriterium, dem sie zugeordnet ist, ausgelassen wird (siehe oben). Einträge zu Understanding-Dokumenten erhalten kein Tag. Jede EN-301-549-Klausel, die das Ergebnis trägt (nur wenn der Scan sie angefordert hat, siehe Andere Normen), erhält ein Tag en301549-<Klausel>, z. B. en301549-9.1.1.1: Klauselnummern sind in jeder Version, die sie enthält, gleich, daher trägt das Tag keine Version, und die Norm oder Checkliste eines Packs wird auf dieselbe Weise unter ihrem eigenen Schlüssel getaggt (die id eines Checklistenpunkts, acme-images, ist sein eigenes Tag). Danach folgen die eigenen meta.tags der Regel (auch bei einer eigenen Regel), ohne die internen Verwaltungs-Tags der Engine und ohne die Kriterien-Tags, die die wcag-<SC>-Tags bereits liefern. |
tool.driver.rules[].helpUri | meta.helpUrl, der Hilfelink der Regel, wenn er eine absolute http(s)-URL ist; sonst das Understanding-Dokument des ersten WCAG-Kriteriums, dem sie zugeordnet ist; fehlt bei einer Regel ohne beides, was auf keine eingebaute Regel zutrifft: Eine Regel ohne Kriterium verlinkt ihren Abschnitt im Regelkatalog. |
tool.driver.version | Die Option toolVersion, falls angegeben (ein Tool, das die Engine einbettet, übergibt seine eigene); sonst engine.version, das Engine-Release, das das Ergebnis erzeugt hat."0.0.0" nur bei einem Ergebnis ohne diesen Wert, aus einem Release vor 1.10.0, oder dessen engine.version kein String ist. |
runs[0].automationDetails.id | Die Option category, mit angehängtem /, falls sie keinen hat (GitHub liest alles bis zum letzten Schrägstrich als Kategorie). Fehlt, wenn keine Kategorie angegeben ist. Siehe Scannen bei mehreren Viewport-Breiten. |
runs[0].properties | wcagVersion, wcagLevel (wenn der Scan eine Stufe hatte: engineOptions.wcagLevel oder die, die sich aus seiner Auswahl ergibt), profile, optInRules und environment (eine Kopie von engine.environment) aus der engine des Ergebnisses: das Konformitätsziel des Laufs, damit ein Dashboard einen WCAG-2.1-Lauf von einem 2.2-Lauf unterscheiden kann und einen AA-Lauf von einem über alle Stufen, die Tags der Opt-in-Regeln, die er über dieses Ziel hinaus hinzugefügt hat, falls vorhanden, und die Bedingungen, unter denen die Seite dargestellt wurde (layout, viewport, devicePixelRatio, colorScheme, forcedColors, fonts und jedes weitere Feld, das sie hat, die der Hooks eingeschlossen; siehe Ausgabeschema). Fehlt bei Ergebnissen aus Releases, die diese Felder noch nicht kennen. |
Ein kompaktes Ergebnis (engineOptions.output.detail: 'findings', siehe Ausgabeschema) wird als das vollständige Ergebnis gelesen, für das es steht, und daher genauso gemeldet. Ein Ergebnis, dessen standards-Liste fehlt oder beschädigt ist, taggt trotzdem jede Norm, die seine Regeln nennen.
Fundorte
Ein DOM-basierter Scan hat keine Zeile oder Spalte zu melden, deshalb ist physicalLocation.artifactLocation.uri das gescannte Ziel selbst, keine Position in einer Quelldatei:
- Scans lokaler Dateien: Eine Datei im aktuellen Arbeitsverzeichnis wird zu einem Pfad relativ dazu, mit Schrägstrichen und prozentkodierten Segmenten (
build%20out/index.html). Entspricht er einer echten Datei in Ihrem Repository, kann GitHub Code Scanning den Befund als Inline-Annotation anzeigen. Eine Datei außerhalb des Arbeitsverzeichnisses wird zu einer absolutenfile:-URL, nicht zu einem relativen Pfad, der mit../aus dem Repository hinausführt und den ein Consumer nicht auflösen könnte. - URL-Scans: die gescannte URL selbst. GitHub Code Scanning listet den Befund trotzdem auf, kann aber keine Inline-Annotation an eine URL hängen, die keine Datei im Repository ist. Das liegt daran, wie SARIF und Code Scanning Befunde mit dem Quellcode verknüpfen, und ist keine Einschränkung von surea11y. Wenn Sie Inline-Annotationen brauchen, scannen Sie die gerenderte HTML-Datei (z. B. ein Build-Artefakt) statt einer Live-URL. Die URL wird so geschrieben, wie ein URL-Parser sie serialisiert; was ein URI nicht aufnehmen kann, wird also prozentkodiert (
https://example.test/a%20b). - Andere Ziele: Ein Ergebnis, dessen
urlkeine URL ist, etwa der Pfad, unter dem eine Seite gescannt wurde (runDomRulesInPage('dist/my page.html', …)), wird zu einem relativen Pfad mit Schrägstrichen und prozentkodierten Segmenten (dist/my%20page.html); ein Windows-Pfad wird zu einer absolutenfile:-URL. Ein Ergebnis ohneurloder mit einer, die kein String ist, wird beiabout:blankverortet.
Der Fundort des Elements (sein Selektor, bei einem Element in einem Shadow Tree mit den Selektoren seiner Shadow Hosts und >>> davor) wird zusätzlich als logicalLocations[].fullyQualifiedName mitgeführt, sodass ein Konsument, der logische Fundorte liest, auch ohne brauchbaren physischen Fundort erfährt, um welches Element es geht.
Frame-übergreifende Ergebnisse
Ein Ergebnis von runa11yCoreAcrossFrames oder aus einem Scan mit .frames(true) eines Bindings (siehe Ausgabeschema) ist ein einziger Lauf: Die Befunde jedes Frames sind Ergebnisse darin. Ein Befund in einem Frame wird im Dokument dieses Frames verortet (artifactLocation.uri ist die URL des Frames) und trägt den Pfad des Frames in properties.frame:
"properties": {"severity": "serious","confidence": "high","reasonCode": "DEFAULT","html": "<img src=\"card.png\">","frame": ["#checkout"]}
Der Pfad ist auch Teil der Identität des Befunds (partialFingerprints), sodass derselbe Mangel in der Seite und in einem Frame, oder in zwei Frames, zwei Alerts sind. Ein Frame, der nicht geantwortet hat (kein Responder, oder eine Zeitüberschreitung), ist ein Hinweis der Stufe warning, "The frame #ads (https://ads.example/) was not scanned: <error>", da darin nichts geprüft wurde.
Scannen bei mehreren Viewport-Breiten
Eine layoutabhängige Regel kann bei einer Viewport-Breite fehlschlagen und bei einer anderen bestehen (siehe Bekannte Einschränkungen), daher sind zwei Breiten einer Seite zwei Analysen, nicht eine. Geben Sie jeder ihre eigene Kategorie:
const { renderSarifReport } = require('@surea11y/core/sarif');for (const width of [390, 1280]) {// ...scan the page at this width into `result`...fs.writeFileSync(`a11y-${width}.sarif`, renderSarifReport(result, { category: `a11y-${width}` }));}
und laden Sie jede Datei einzeln hoch. GitHub Code Scanning behandelt jede Kategorie als eigene Analyse. Innerhalb einer Kategorie ersetzt ein späterer Upload den früheren, und ein Alert, der darin fehlt, wird geschlossen; zwei Breiten, die unter derselben Kategorie hochgeladen werden, würden einen breitenspezifischen Alert also bei jedem Lauf öffnen und wieder schließen. GitHub lehnt außerdem eine einzelne SARIF-Datei ab, die zwei Runs desselben Tools und derselben Kategorie enthält. github/codeql-action/upload-sarif hat einen Input category, der dieselbe Aufgabe übernimmt, wenn die Datei keine Kategorie trägt.
Die Identität eines Befunds enthält den Viewport nicht und muss es auch nicht: Dasselbe Element mit demselben Mangel ist bei jeder Breite derselbe Befund, und die Kategorie hält die Analysen auseinander. runs[0].properties.environment hält den Viewport fest, den jeder Lauf verwendet hat.
Optionen
renderSarifReport(result, options) aus @surea11y/core/sarif gibt das Log als JSON-Text zurück. Es nimmt:
| Option | Standard | Wirkung |
|---|---|---|
toolVersion | engine.version | tool.driver.version, für ein Tool, das die Engine einbettet. |
informationUri | 'https://github.com/SureA11y/core' | tool.driver.informationUri, die Startseite des Tools. |
baselineEntries | keine | Die Baseline-Datei wie gespeichert (JSON.parse davon) oder ihre entries-Liste, siehe Dateiformat. Dort erfasste fail-Vorkommen werden weggelassen, wie es --baseline tut (siehe unten). Ein Eintrag, den 1.13 unter einem Reason-Code geschrieben hat, der sich in 1.14 geändert hat, passt weiterhin zum Befund unter seinem neuen Code. |
category | keine | runs[0].automationDetails.id, siehe Scannen bei mehreren Viewport-Breiten. |
leanFailAs | 'warning' | 'error' meldet einen cantTell-Befund, dessen Tendenz zu fail weist, als error, sodass ein Build, der auf Fehler prüft, auch bei wahrscheinlichen Fehlern scheitert. Groß- und Kleinschreibung spielt keine Rolle; ein anderer Wert wirft einen TypeError. Das gilt auch für eine Best-Practice-Regel: Eine solche Regel, keinem WCAG-Kriterium zugeordnet, ergibt nie fail, und die Fakten, die sie feststellt, tendieren ohne Bedingung zu fail; eine übersprungene Überschriftenebene (heading-order) ist also ein error. Ein Befund einer Regel, deren Kriterien alle außerhalb des Ziels des Scans liegen, bleibt ein note. Ein Befund, der zu fail tendiert und in baselineEntries einen mit lean: 'fail' gekennzeichneten Eintrag hat, bleibt ein warning. |
leanFailWcagOnly | false | Mit leanFailAs: 'error' hebt true nur die Befunde von Regeln an, die einem WCAG-Kriterium im Ziel des Scans zugeordnet sind, und lässt die Tendenzen der Best-Practice-Regeln bei warning. Ein anderer Wert als true oder false wirft einen TypeError. leaningFailures(result, { wcagOnly }) aus @surea11y/core listet die Befunde, die diese beiden Optionen anheben, vor Anwendung der Baseline. |
packs | keine | Die Packs, die der Scan ausgeführt hat (engineOptions.packs): In Node führen die offenen Befunde einer Pack-Regel die Schritte ihres eigenen meta.review mit (properties.howToCheck). |
Kombination mit --baseline
Ein generischer SARIF-Konsument kennt kein eigenes Konzept für „bekannt, nicht daran scheitern“. Die einzige getreue Art, eine Baseline in der SARIF-Ausgabe zu berücksichtigen, ist daher, bereits bekannte fail-Vorkommen ganz wegzulassen, statt sie auf warning herabzustufen:
$ surea11y scan ./dist/index.html --baseline baseline.json --sarif results.sarif
cantTell-Vorkommen werden nie durch eine Baseline weggelassen. Eine Baseline, die mit buildBaselineEntries(result, { leanFail: true }) geschrieben wurde, erfasst auch die Befunde, die zu fail tendieren, als mit lean: 'fail' gekennzeichnete Einträge (siehe Baseline / Allowlist); unter leanFailAs: 'error' bleibt ein solcher Befund ein warning, statt zu error angehoben zu werden. So kann ein Team die wahrscheinlichen Fehler, die es schon hat, akzeptieren und trotzdem bei neuen scheitern lassen. --write-baseline erfasst nur fail-Vorkommen.
Kombination mit --html/--json
--sarif, --html und --json sind unabhängige Ausgabe-Flags: Übergeben Sie in einem Lauf jede beliebige Kombination; jedes schreibt bzw. gibt seinen eigenen Bericht aus demselben einzelnen Scan aus.