Rapport SARIF

--sarif <path> écrit un journal SARIF 2.1.0 (le format standard qu’attendent GitHub Code Scanning et les autres tableaux de bord compatibles SARIF) à la place du résultat brut du moteur produit par --json, ou en plus de celui-ci.

$ surea11y scan ./dist/index.html --sarif results.sarif

Voir Intégration CI pour un workflow GitHub Actions prêt à copier qui lance une analyse et envoie results.sarif vers l’onglet « Security ».

Pourquoi un format distinct de --json

Le résultat brut de --json (voir Schéma de sortie) est le contrat propre à ce moteur, versionné et stable selon API_STABILITY.md. SARIF est un contrat différent, défini à l’extérieur et conçu spécialement pour les tableaux de bord d’analyse de code : une entrée de checksResults[] et un result SARIF ne se correspondent pas un à un, donc il s’agit d’une véritable conversion, pas d’une simple re-sérialisation.

Ce qui devient un résultat SARIF

Seules les occurrences fail/cantTell produisent des résultats SARIF (la même approche « violations uniquement » que le rapport HTML). Un journal sans résultat indique donc que rien n’a échoué ni ne demande de revue parmi les vérifications exécutées ; il ne dit pas ce qui a été vérifié, et ce n’est pas une déclaration de conformité. Conservez le résultat JSON pour cela.

Une vérification notApplicable n’est pas toujours vide : une règle peut joindre une occurrence expliquant pourquoi elle n’avait rien à juger, ce que font les règles de contraste lorsqu’aucun texte n’avait d’arrière-plan calculable. Ces occurrences ne deviennent jamais des résultats (un consommateur traite chaque résultat comme une alerte, et « ceci n’a pas été évalué » n’en est pas une), mais elles ne sont pas non plus abandonnées. Elles sont transmises comme entrées de niveau note dans runs[0].invocations[0].toolExecutionNotifications, chacune indiquant la règle dont elle provient via 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" }
}
]
}
]

Cela évite qu’un pipeline uniquement SARIF interprète le silence comme un certificat de bonne santé : l’absence d’alerte de contraste peut signifier que la page est correcte, ou que le contraste n’a jamais pu être calculé, et seule la notice permet de distinguer les deux. Le bloc n’est émis que s’il y a quelque chose à signaler : une exécution sans rien à rapporter n’a donc aucune clé invocations. Une règle qui a réussi n’écrit aucune notification.

Une règle qui ne s’est pas terminée (elle a levé une exception : cantTell sans occurrence et avec une error) n’a rien jugé : ce n’est donc pas non plus un résultat. C’est une notification de niveau error dans la même liste, "The rule <id> did not complete: <error>", qui nomme la règle dans associatedRule.id, pour que le trou dans la couverture ne se lise pas comme une réussite.

Résultat du moteurNiveau SARIFSignification
failerrorViolation déterministe : le cas qui bloque la CI.
cantTellwarningNécessite une revue humaine : signalé, mais ne devrait pas à lui seul bloquer un build.
cantTell penchant vers fail, avec leanFailAs: 'error'errorNon tranché, mais les indices de la page pointent vers un échec (lean) ; compté par un contrôle qui bloque sur les erreurs.

Un constat non tranché qui a une tendance la porte dans properties.lean : { "toward": "fail", "why": "The heading skips a level after the one before it." }, quel que soit le niveau auquel il est signalé. Lorsque la tendance en a, elle porte aussi caution, ce qui pourrait la rendre fausse ("A layout table can also hold numbers."), et measured, la fréquence à laquelle elle a eu raison ({ "agreed": 47, "cases": 50, "corpus": "…", "coreVersion": "…" }, une fois qu’une tendance a été comptée sur cinq cas, ce qui n’est encore le cas d’aucune), si bien qu’une alerte montre ce que le rapport HTML montre à côté de la tendance. Ses clés signals et i18n restent dans le résultat de l’analyse.

Une règle dont chaque critère WCAG est hors de la cible de l’analyse (une règle AAA sous engineOptions.wcagLevel: 'AA', ou duplicate-id sous WCAG 2.2 : les critères que l’analyse a écartés de rulesResults, comme pour les tags wcag-<SC> ci-dessous) s’est quand même exécutée, et ses constats fail et cantTell restent des résultats, au niveau note, pour qu’ils soient visibles sans faire échouer un build limité à la cible. Chacun en donne la raison dans properties.outsideTarget : { "criteria": ["1.4.6"], "target": "WCAG 2.2 AA" }.

Chaque règle exécutée (qu’elle ait produit un résultat ou non) est listée une fois dans runs[0].tool.driver.rules, avec defaultConfiguration.level défini d’après le type de la règle : automatic (peut renvoyer fail) → error, manual (jamais fail) → warning. Un id de règle qui n’est pas une chaîne, dans un résultat enregistré, est écrit sous forme de texte ; une vérification sans id de règle ne nomme aucune règle, et elle est omise, comme JUnit et EARL l’omettent.

Correspondance des champs

Champ SARIFSource
results[].ruleIdchecksResults[i].ruleId
results[].message.textoccurrence.summary + occurrence.hint. Une occurrence sans résumé (celle d’une règle personnalisée, par exemple) utilise le titre de la règle, ou son id, puisque GitHub rejette un résultat dont le texte est vide. Les étapes d’un constat non tranché suivent, après une ligne vide : How to check:, les étapes numérotées, puis Passes when: et Fails when:, pour qu’une alerte dise comment y répondre.
results[].properties.howToCheckLes étapes d’un constat non tranché, { steps, passWhen, failWhen }, telles que les donne occurrence.howToCheck. Un résultat qui arrive sans elles, comme celui d’un binding, les reçoit dans Node ; packs (les packs que l’analyse a exécutés) ajoute celles propres à une règle de pack. Comme la tendance, elles ne font pas partie de l’identité du constat.
results[].locations[].physicalLocation.artifactLocation.uriLa cible analysée ; voir Emplacements ci-dessous.
results[].locations[].logicalLocations[].fullyQualifiedNameL’emplacement de l’élément, avec kind: "element", lorsque occurrence.selector est une chaîne non vide. Hors d’un arbre shadow, c’est le sélecteur. Pour un élément situé dans un arbre shadow, ce sont les sélecteurs des shadow hosts, puis celui de l’élément, joints par >>>  : html > body > main > x-card >>> button. La ligne selector: de JUnit et le rapport HTML affichent le même texte.
results[].partialFingerprints["surea11y/violation/v2"]Le condensé SHA-256, en 64 chiffres hexadécimaux, de la même clé d’identité ruleId + reasonCode + html que celle utilisée par Baseline / allowlist (computeBaselineKey) : une empreinte stable fondée sur le contenu plutôt que sur la position, pour les consommateurs qui la lisent ; le même constat reçoit le même condensé dans chaque rapport. Avant la version 1.11, surea11y/violation/v1 contenait la clé elle-même : un outil qui lisait v1 voit donc chaque constat comme nouveau, une seule fois. Un constat dont le code de raison change reçoit un autre condensé : en 1.14, les constats des dix-sept règles qui donnent désormais un code, et ceux des codes scindés selon leur cause ; leurs alertes Code Scanning se ferment donc une fois et les constats reviennent comme nouveaux (voir Baseline / allowlist).
results[].partialFingerprints.primaryLocationLineHashLes 16 premiers chiffres hexadécimaux du condensé et un compteur, <16 hex digits>:<n> (inchangé par rapport à avant v2), où n compte un constat répété sur la page (le même composant défectueux deux fois). GitHub Code Scanning fait correspondre les alertes d’un envoi à l’autre sur ce seul champ. Un constat issu du DOM n’a pas de ligne que l’action d’envoi de GitHub pourrait hacher ; sans ce champ, les alertes étaient donc mises en correspondance par position, et corriger un constat pouvait fermer l’alerte d’un autre. Si vous venez de core 1.8 ou d’une version antérieure, le premier envoi ferme les alertes existantes et les rouvre comme nouvelles, une seule fois ; une alerte ignorée dans GitHub doit l’être à nouveau.
results[].properties.severity / .confidence / .reasonCode / .htmlLes severity/confidence du résultat lui-même, plus le même reasonCode et le même extrait html sur lesquels repose l’empreinte ci-dessus ; à titre informatif, hors du schéma propre à SARIF.
tool.driver.rules[].properties.tagsaccessibility, automatic/manual, et un tag wcag-<SC>par critère de succès WCAG présent dans meta.normativeMappings que le niveau et la version WCAG de l’analyse conservent. Un critère que l’analyse a écarté de rulesResults à cause d’eux ne reçoit pas de tag : un critère AAA sous engineOptions.wcagLevel: 'AA', ou 4.1.1 quand includeRuleIds: ['duplicate-id'] s’est exécuté sous WCAG 2.2. Un critère que l’analyse signale, comme une agrégation nommée par son id au-dessus du niveau, garde son tag, tout comme chaque critère d’un résultat qui n’enregistre ni engine.wcagLevel ni engine.wcagVersion. La règle reste, et ses résultats aussi, en notes quand chaque critère auquel elle est associée est écarté (voir plus haut). Les entrées de documents Understanding n’ont pas de tag. Chaque clause d’EN 301 549 que porte le résultat (seulement quand l’analyse les a demandées, voir Autres normes) reçoit un tag en301549-<clause>, p. ex. en301549-9.1.1.1 : les numéros de clause sont les mêmes dans toutes les versions qui les contiennent, le tag ne porte donc pas de version, et la norme ou la liste de contrôle d’un pack reçoit des tags de la même façon, sous sa propre clé (l’id d’un point de la liste de contrôle, acme-images, est son propre tag). Viennent ensuite les meta.tags de la règle elle-même (y compris ceux d’une règle personnalisée), sans les tags internes du moteur ni les tags de critère que les tags wcag-<SC> donnent déjà.
tool.driver.rules[].helpUrimeta.helpUrl, le lien d’aide de la règle, quand c’est une URL http(s) absolue ; sinon le document Understanding du premier critère WCAG auquel elle est associée ; absent pour une règle qui n’a ni l’un ni l’autre, ce qui n’est le cas d’aucune règle intégrée : une règle associée à aucun critère renvoie à sa section du catalogue des règles.
tool.driver.versionL’option toolVersion, si elle est fournie (un outil qui enveloppe le moteur passe la sienne) ; sinon engine.version, la version du moteur qui a produit le résultat."0.0.0" seulement pour un résultat qui n’en a pas, issu d’une version antérieure à 1.10.0, ou dont l’engine.version n’est pas une chaîne.
runs[0].automationDetails.idL’option category, avec un / final ajouté s’il n’y en a pas (GitHub lit tout ce qui précède la dernière barre oblique comme la catégorie). Absent lorsqu’aucune catégorie n’est fournie. Voir Analyser à plusieurs largeurs de viewport.
runs[0].propertieswcagVersion, wcagLevel (quand l’analyse avait un niveau : engineOptions.wcagLevel, ou celui que donne sa sélection), profile, optInRules et environment (une copie de engine.environment) issus de l’engine du résultat : la cible de conformité utilisée par l’exécution, pour qu’un tableau de bord distingue une exécution WCAG 2.1 d’une exécution 2.2, et une exécution AA d’une exécution à tous les niveaux, les tags de règles optionnelles qu’elle a ajoutés au-delà de cette cible, s’il y en a, et les conditions dans lesquelles la page a été rendue (layout, viewport, devicePixelRatio, colorScheme, forcedColors, fonts et tout autre champ qu’il contient, y compris ceux des hooks ; voir Schéma de sortie). Omis pour les résultats de versions antérieures à ces champs.

Un résultat compact (engineOptions.output.detail: 'findings', voir Schéma de sortie) est lu comme le résultat complet qu’il représente ; il est donc rapporté de la même façon. Un résultat dont la liste standards est absente ou endommagée reçoit quand même un tag pour chaque norme que nomment ses règles.

Emplacements

Une analyse fondée sur le DOM n’a pas de ligne ni de colonne à rapporter, donc physicalLocation.artifactLocation.uri est la cible analysée elle-même, et non une position dans un fichier source :

  • Analyse de fichiers locaux : un fichier situé dans le répertoire de travail courant devient un chemin relatif à celui-ci, avec des barres obliques et chaque segment encodé en pourcentage (build%20out/index.html). S’il correspond à un vrai fichier de votre dépôt, GitHub Code Scanning peut afficher le résultat sous forme d’annotation en ligne. Un fichier situé hors du répertoire de travail devient une URL file: absolue, et non un chemin relatif qui sort du dépôt avec ../, qu’un consommateur ne pourrait pas résoudre.
  • Analyse d’URL : l’URL analysée elle-même. GitHub Code Scanning listera quand même le résultat, mais ne peut pas associer une annotation en ligne à une URL qui n’est pas un fichier du dépôt : cela tient à la façon dont SARIF et Code Scanning rattachent les résultats au code source, ce n’est pas une limite de surea11y. Si vous avez besoin d’annotations en ligne, analysez le fichier HTML rendu (par exemple un artefact de build) plutôt qu’une URL en direct. L’URL est écrite telle qu’un analyseur d’URL la sérialise : ce qu’une URI ne peut pas contenir est encodé en pourcentage (https://example.test/a%20b).
  • Autres cibles : un résultat dont l’url n’est pas une URL, comme le chemin sous lequel une page a été analysée (runDomRulesInPage('dist/my page.html', …)), devient un chemin relatif, avec des barres obliques et chaque segment encodé en pourcentage (dist/my%20page.html) ; un chemin Windows devient une URL file: absolue. Un résultat sans url, ou dont l’url n’est pas une chaîne, est situé à about:blank.

L’emplacement de l’élément (son sélecteur, précédé des sélecteurs de ses shadow hosts et de >>> lorsqu’il est situé dans un arbre shadow) est également transmis comme logicalLocations[].fullyQualifiedName, de sorte qu’un consommateur qui lit les emplacements logiques sait tout de même « quel élément » est concerné, même sans emplacement physique exploitable.

Résultats multi-frames

Un résultat de runa11yCoreAcrossFrames, ou d’une analyse .frames(true) d’un binding (voir Schéma de sortie), forme une seule exécution : les constats de chaque frame en sont des résultats. Un constat situé dans un frame est localisé dans le document de ce frame (artifactLocation.uri est l’URL du frame), et porte le chemin du frame dans properties.frame :

"properties": {
"severity": "serious",
"confidence": "high",
"reasonCode": "DEFAULT",
"html": "<img src=\"card.png\">",
"frame": ["#checkout"]
}

Le chemin fait aussi partie de l’identité du constat (partialFingerprints) : le même défaut dans la page et dans un frame, ou dans deux frames, donne donc deux alertes. Un frame qui n’a pas répondu (pas de responder, ou délai dépassé) est une notification de niveau warning, "The frame #ads (https://ads.example/) was not scanned: <error>", puisque rien n’y a été vérifié.

Analyser à plusieurs largeurs de viewport

Une règle qui dépend de la mise en page peut échouer à une largeur de viewport et réussir à une autre (voir Limites connues) ; une page analysée à deux largeurs correspond donc à deux analyses, pas à une seule. Donnez à chacune sa propre catégorie :

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}` }));
}

puis envoyez chaque fichier séparément. GitHub Code Scanning traite chaque catégorie comme une analyse à part entière. Au sein d’une même catégorie, un envoi ultérieur remplace le précédent, et une alerte qui n’y figure plus est fermée : deux largeurs envoyées sous la même catégorie ouvriraient et fermeraient donc à chaque exécution une alerte propre à une largeur. GitHub rejette aussi un fichier SARIF unique contenant deux exécutions du même outil et de la même catégorie. github/codeql-action/upload-sarif a une entrée category qui joue le même rôle lorsque le fichier n’en porte pas.

L’identité du constat n’inclut pas le viewport, et n’en a pas besoin : le même élément avec le même défaut est le même constat à n’importe quelle largeur, et c’est la catégorie qui sépare les analyses. runs[0].properties.environment enregistre le viewport utilisé par chaque exécution.

Options

renderSarifReport(result, options), de @surea11y/core/sarif, renvoie le journal sous forme de texte JSON. Elle accepte :

OptionPar défautEffet
toolVersionengine.versiontool.driver.version, pour un outil qui enveloppe le moteur.
informationUri'https://github.com/SureA11y/core'tool.driver.informationUri, la page d’accueil de l’outil.
baselineEntriesaucuneLe fichier de baseline tel qu’enregistré (son JSON.parse) ou sa liste entries, voir Format du fichier. Les occurrences fail qui y sont enregistrées sont omises, comme le fait --baseline (voir ci-dessous). Une entrée écrite par 1.13 sous un code de raison qui a changé en 1.14 correspond toujours au constat sous son nouveau code.
categoryaucuneruns[0].automationDetails.id, voir Analyser à plusieurs largeurs de viewport.
leanFailAs'warning''error' signale au niveau error un constat cantTell dont la tendance pointe vers l’échec, si bien qu’un build bloqué sur les erreurs échoue aussi sur les échecs probables. Lue quelle que soit la casse ; toute autre valeur lève une TypeError. Y compris pour une règle de bonne pratique : une telle règle, associée à aucun critère WCAG, n’échoue jamais, et les faits qu’elle constate penchent vers l’échec sans condition, si bien qu’un niveau de titre sauté (heading-order) devient une error. Le constat d’une règle dont chaque critère est hors de la cible de l’analyse reste une note. Un constat qui penche vers l’échec et a une entrée marquée lean: 'fail' dans baselineEntries reste un warning.
leanFailWcagOnlyfalseAvec leanFailAs: 'error', true ne relève que les constats des règles associées à un critère WCAG de la cible de l’analyse, en laissant les tendances des règles de bonne pratique au niveau warning. Une valeur autre que true ou false lève une TypeError. leaningFailures(result, { wcagOnly }), de @surea11y/core, liste les constats que ces deux options relèvent, avant la baseline.
packsaucuneLes packs que l’analyse a exécutés (engineOptions.packs) : dans Node, les constats non tranchés d’une règle de pack portent les étapes de son propre meta.review (properties.howToCheck).

Combiner avec --baseline

Un consommateur SARIF générique n’a pas de notion propre de « connu, ne pas bloquer dessus » : la seule façon fidèle d’appliquer une baseline dans une sortie SARIF est d’omettre entièrement les occurrences fail déjà connues, plutôt que de les rétrograder en warning :

$ surea11y scan ./dist/index.html --baseline baseline.json --sarif results.sarif

Les occurrences cantTell ne sont jamais omises par une baseline. Une baseline écrite avec buildBaselineEntries(result, { leanFail: true }) enregistre aussi les constats qui penchent vers l’échec, sous forme d’entrées marquées lean: 'fail' (voir Baseline / allowlist) ; avec leanFailAs: 'error', un tel constat reste un warning au lieu d’être relevé en error, si bien qu’une équipe peut accepter les échecs probables qu’elle a déjà et continuer à bloquer sur les nouveaux. --write-baseline n’enregistre que les occurrences fail.

Combiner avec --html/--json

--sarif, --html et --json sont des options de sortie indépendantes : combinez-les librement dans une même exécution ; chacune écrit ou affiche son propre rapport à partir de la même analyse unique.