Informe SARIF

--sarif <path> escribe un registro SARIF 2.1.0 (el formato estándar que espera GitHub Code Scanning y otros paneles que consumen SARIF), en lugar del resultado sin procesar del motor de --json, o además de él.

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

Ver integración con CI para un flujo de trabajo de GitHub Actions listo para usar que ejecuta un análisis y sube results.sarif a la pestaña "Security".

Por qué un formato separado de --json

El resultado sin procesar de --json (ver Esquema de salida) es el contrato propio de este motor, versionado y estable según API_STABILITY.md. SARIF es un contrato distinto, definido externamente, diseñado específicamente para paneles de análisis de código: una entrada de checksResults[] y un result de SARIF no se corresponden 1:1, así que esto es una conversión real, no una re-serialización.

Qué se convierte en un resultado SARIF

Solo las ocurrencias fail/cantTell producen resultados SARIF (el mismo enfoque de "solo violaciones" que el informe HTML). Así que un registro sin resultados dice que nada falló ni necesita revisión entre las comprobaciones que se ejecutaron; no dice qué se comprobó, y no es una declaración de conformidad. Para eso, conserva el resultado JSON.

Una comprobación notApplicable no siempre está vacía: una regla de un pack o personalizada puede adjuntar una ocurrencia que explica por qué no tuvo nada que juzgar (las reglas de core no adjuntan ninguna, desde la 1.16.0). Esas nunca se convierten en resultados: un consumidor trata cada resultado como una alerta, y "esto no se evaluó" no lo es. Pero tampoco se descartan. Se incluyen como entradas de nivel note en runs[0].invocations[0].toolExecutionNotifications, cada una identificando la regla de la que proviene mediante associatedRule.id:

"invocations": [
{
"executionSuccessful": true,
"toolExecutionNotifications": [
{
"level": "note",
"message": { "text": "No form on this page could be checked: each is inside a closed shadow root." },
"associatedRule": { "id": "acme-form-review" }
}
]
}
]

Esto evita que un flujo que solo lee SARIF interprete el silencio como un buen estado de salud. Las reglas de contraste de core no necesitan ninguna notificación para ello: el texto cuyo contraste no se pudo calcular es un resultado de contrast-computable. El bloque se emite solo cuando hay algo que decir, así que una ejecución sin nada que indicar no tiene ninguna clave invocations. Una regla que pasó no escribe ninguna notificación.

Una regla que no terminó (lanzó una excepción: cantTell sin ocurrencias y con un error) no juzgó nada, así que tampoco es un resultado. Es una notificación de nivel error en la misma lista, "The rule <id> did not complete: <error>", que nombra la regla en associatedRule.id, para que el hueco en la cobertura no se lea como un aprobado.

Resultado del motorNivel SARIFSignificado
failerrorViolación determinista: el caso de bloqueo en CI.
cantTellwarningRequiere revisión humana: se muestra, pero no debería bloquear una compilación por sí sola.
cantTell con tendencia a fail, con leanFailAs: 'error'errorSin decidir, pero los indicios de la página apuntan a un fallo (lean); lo cuenta una puerta sobre errores.

Un hallazgo sin decidir con tendencia la lleva en properties.lean: { "toward": "fail", "why": "The heading skips a level after the one before it." }, sea cual sea el nivel con el que se informe. Cuando la tendencia los tiene, también lleva caution, qué podría hacer que se equivoque ("A layout table can also hold numbers."), y measured, cuántas veces ha acertado ({ "agreed": 47, "cases": 50, "corpus": "…", "coreVersion": "…" }, cuando una tendencia se ha contado en cinco casos, lo que aún no ha ocurrido con ninguna), para que una alerta muestre lo que el informe HTML muestra junto a la tendencia. Sus claves signals e i18n se quedan en el resultado del análisis.

Una regla cuyos criterios de WCAG quedan todos fuera del objetivo del análisis (una regla AAA con engineOptions.wcagLevel: 'AA', o duplicate-id con WCAG 2.2: los criterios que el análisis dejó fuera de rulesResults, como para las etiquetas wcag-<SC> más abajo) se ejecutó igualmente, y sus hallazgos fail y cantTell siguen siendo resultados, con nivel note, para que se vean sin hacer fallar una compilación limitada al objetivo. Cada uno dice por qué en properties.outsideTarget: { "criteria": ["1.4.6"], "target": "WCAG 2.2 AA" }.

Cada regla que se ejecutó (sin importar si produjo un resultado) se lista una vez en runs[0].tool.driver.rules, con defaultConfiguration.level establecido según el type de la regla: automatic (capaz de fallar) → error, manual (nunca fail) → warning. Un id de regla que no es una cadena, en un resultado guardado, se escribe como su texto; una comprobación sin id de regla no nombra ninguna regla y se omite, como la omiten JUnit y EARL.

Correspondencia de campos

Campo SARIFOrigen
results[].ruleIdchecksResults[i].ruleId
results[].message.textoccurrence.summary + occurrence.hint. Una ocurrencia sin resumen (la de una regla personalizada, por ejemplo) usa el título de la regla, o su id, ya que GitHub rechaza un resultado con el texto vacío. Detrás, tras una línea en blanco, van los pasos de un hallazgo sin decidir: How to check:, los pasos numerados, y luego Passes when: y Fails when:, para que una alerta diga cómo responderla.
results[].properties.howToCheckLos pasos de un hallazgo sin decidir, { steps, passWhen, failWhen }, tal como los da occurrence.howToCheck. Un resultado que llega sin ellos, como el de un binding, los recibe en Node; packs (los packs que ejecutó el análisis) añade los propios de una regla de un pack. Como la tendencia, no forma parte de la identidad del hallazgo.
results[].locations[].physicalLocation.artifactLocation.uriEl objetivo analizado (ver Ubicaciones más abajo).
results[].locations[].logicalLocations[].fullyQualifiedNameLa ubicación del elemento, con kind: "element", cuando occurrence.selector es una cadena no vacía. Fuera de un árbol shadow es el selector. Para un elemento dentro de un árbol shadow son los selectores de los hosts shadow y luego el del propio elemento, unidos con >>> : html > body > main > x-card >>> button. La línea selector: de JUnit y el informe HTML muestran el mismo texto.
results[].partialFingerprints["surea11y/violation/v2"]El resumen SHA-256, de 64 dígitos hexadecimales, de la misma clave de identidad ruleId + reasonCode + html que usa Baseline / allowlist (computeBaselineKey): una huella basada en el contenido y estable, no basada en la posición, para los consumidores que la leen: el mismo hallazgo recibe el mismo resumen en todos los informes. Antes de la 1.11, surea11y/violation/v1 contenía la propia clave, así que una herramienta que leía v1 ve cada hallazgo como nuevo una vez. Un hallazgo cuyo código de motivo cambia recibe otro resumen: en la 1.14, los hallazgos de las diecisiete reglas que ahora dan un código, y los de los códigos divididos según su causa, así que sus alertas de Code Scanning se cierran una vez y los hallazgos vuelven como nuevos (ver Baseline / allowlist).
results[].partialFingerprints.primaryLocationLineHashLos primeros 16 dígitos hexadecimales del resumen y un recuento, <16 hex digits>:<n> (sin cambios respecto a antes de v2), donde n cuenta un hallazgo repetido en la página (el mismo componente defectuoso dos veces). GitHub Code Scanning empareja las alertas entre subidas solo por este campo. Un hallazgo del DOM no tiene ninguna línea a la que la acción de subida de GitHub pueda aplicar el hash, así que sin él las alertas se emparejaban por posición, y corregir un hallazgo podía cerrar la alerta de otro. Si vienes de core 1.8 o anterior, la primera subida cierra las alertas existentes y las vuelve a abrir como nuevas, una sola vez; un descarte hecho en GitHub hay que volver a hacerlo.
results[].properties.severity / .confidence / .reasonCode / .htmlEl propio severity/confidence del resultado, más el mismo reasonCode y fragmento html sobre el que se calcula la huella anterior: informativo, no forma parte del propio esquema de SARIF.
tool.driver.rules[].properties.tagsaccessibility, automatic/manual,y una etiqueta wcag-<SC> por cada criterio de conformidad de WCAG en meta.normativeMappings que conservan el nivel y la versión de WCAG del análisis. Un criterio que el análisis dejó fuera de rulesResults por ellos no recibe etiqueta: un criterio AAA con engineOptions.wcagLevel: 'AA', o 4.1.1 cuando includeRuleIds: ['duplicate-id'] se ejecutó con WCAG 2.2. Un criterio que el análisis informa, como un resultado agregado nombrado por id por encima del nivel, conserva su etiqueta, igual que todos los criterios de un resultado que no registra ni engine.wcagLevel ni engine.wcagVersion. La regla se mantiene, y sus resultados también, como notas cuando todos los criterios a los que se asocia quedan fuera (ver más arriba). Las entradas de documentos Understanding no reciben etiqueta. Cada cláusula de EN 301 549 que lleve el resultado (solo cuando el análisis las pidió, ver Otros estándares) recibe una etiqueta en301549-<cláusula>, por ejemplo en301549-9.1.1.1: los números de cláusula son los mismos en todas las versiones que los tienen, así que la etiqueta no lleva versión, y el estándar o la lista de comprobación de un pack se etiqueta de la misma forma bajo su propia clave (el id de un elemento de lista de comprobación, acme-images, es su propia etiqueta). Después, las propias meta.tags de la regla (incluidas las de una regla personalizada), sin las etiquetas internas del motor ni las etiquetas de criterio que ya dan las etiquetas wcag-<SC>.
tool.driver.rules[].helpUrimeta.helpUrl, el enlace de ayuda de la regla, cuando es una URL http(s) absoluta; si no, el documento Understanding del primer criterio de WCAG al que se asocia; ausente en una regla sin ninguno de los dos, lo que no ocurre con ninguna regla incorporada: una sin criterio asociado enlaza su sección del catálogo de reglas.
tool.driver.versionLa opción toolVersion, si se indica (una herramienta que envuelve el motor pasa la suya); si no, engine.version, la versión del motor que produjo el resultado."0.0.0" solo para un resultado que no lo tiene, de una versión anterior a la 1.10.0, o cuyo engine.version no es una cadena.
runs[0].automationDetails.idLa opción category, con una / final añadida si no la tiene (GitHub lee todo lo que hay hasta la última barra como la categoría). Ausente cuando no se indica ninguna categoría. Ver Analizar con varios anchos de viewport.
runs[0].propertieswcagVersion, wcagLevel (cuando el análisis tenía un nivel: engineOptions.wcagLevel, o el que da su selección), profile (el primer perfil, en un análisis de varios), profiles (solo en un análisis de varios perfiles: cada perfil que ejecutó, engine.profiles), optInRules y environment (una copia de engine.environment) del engine del resultado: el objetivo de conformidad que usó la ejecución, para que un panel distinga una ejecución de WCAG 2.1 de una de 2.2, y una ejecución AA de una de todos los niveles, las etiquetas de reglas opcionales que añadió más allá de ese objetivo, cuando añadió alguna, y las condiciones en que se renderizó la página (layout, viewport, devicePixelRatio, colorScheme, forcedColors, fonts y cualquier otro campo que tenga, incluidos los de los hooks; ver Esquema de salida). Se omiten en resultados de versiones anteriores a esos campos.

Un resultado compacto (engineOptions.output.detail: 'findings', ver Esquema de salida) se lee como el resultado completo que representa, así que se informa igual. Un resultado cuya lista standards falta o está dañada sigue etiquetando todos los estándares que nombran sus reglas.

Ubicaciones

El análisis basado en el DOM no tiene línea/columna que indicar, así que physicalLocation.artifactLocation.uri es el propio objetivo analizado, no una posición en un archivo fuente:

  • Análisis de archivos locales: un archivo dentro del directorio de trabajo actual es una ruta relativa a él, con barras diagonales y cada segmento codificado con porcentajes (build%20out/index.html). Si esto coincide con un archivo real de tu repositorio, GitHub Code Scanning puede mostrar el hallazgo como una anotación en línea. Un archivo fuera del directorio de trabajo es una URL file: absoluta, no una ruta relativa que sale del repositorio con ../, que un consumidor no podría resolver.
  • Análisis de URL: la propia URL analizada. GitHub Code Scanning seguirá listando el hallazgo, pero no puede adjuntar una anotación en línea a una URL que no es un archivo del repositorio: esto es inherente a cómo SARIF/Code Scanning asocian los hallazgos con el código fuente, no una limitación de surea11y. Si necesitas anotaciones en línea, analiza el archivo HTML renderizado (por ejemplo, un artefacto de compilación) en lugar de una URL en vivo. La URL se escribe tal como la serializa un analizador de URL, así que lo que una URI no admite se codifica con porcentajes (https://example.test/a%20b).
  • Otros objetivos: un resultado cuyo url no es una URL, como la ruta con la que se analizó una página (runDomRulesInPage('dist/my page.html', …)), es una ruta relativa, con barras normales y cada segmento codificado con porcentajes (dist/my%20page.html); una ruta de Windows es una URL file: absoluta. Un resultado sin url, o con uno que no es una cadena, se ubica en about:blank.

La ubicación del elemento (su selector, precedido de los selectores de sus hosts shadow y >>> cuando está dentro de un árbol shadow) se incluye además como un logicalLocations[].fullyQualifiedName, así que un consumidor que lea las ubicaciones lógicas igual obtiene la señal de "qué elemento" incluso sin una ubicación física utilizable.

Resultados entre frames

Un resultado de runa11yCoreAcrossFrames, o de un análisis .frames(true) de una integración (ver Esquema de salida), es una sola ejecución: los hallazgos de cada frame son resultados dentro de ella. Un hallazgo en un frame se ubica en el documento de ese frame (artifactLocation.uri es la URL del frame), y lleva la ruta del frame en properties.frame:

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

La ruta también forma parte de la identidad del hallazgo (partialFingerprints), así que el mismo defecto en la página y en un frame, o en dos frames, son dos alertas. Un frame que no respondió (sin respondedor, o se agotó el tiempo) es una notificación de nivel warning, "The frame #ads (https://ads.example/) was not scanned: <error>", ya que no se comprobó nada en él.

Analizar con varios anchos de viewport

Una regla que depende del diseño puede fallar con un ancho de viewport y cumplirse con otro (ver Limitaciones conocidas), así que una página analizada con dos anchos son dos análisis, no uno. Da a cada uno su propia categoría:

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

y sube cada archivo por separado. GitHub Code Scanning trata cada categoría como un análisis propio. Dentro de una categoría, una subida posterior sustituye a la anterior, y una alerta que no aparece en ella se cierra, así que dos anchos subidos con la misma categoría abrirían y cerrarían una alerta propia de un ancho en cada ejecución. GitHub también rechaza un único archivo SARIF que contenga dos ejecuciones de la misma herramienta y la misma categoría. github/codeql-action/upload-sarif tiene una entrada category que hace lo mismo cuando el archivo no lleva ninguna.

La identidad del hallazgo no incluye el viewport, y no le hace falta: el mismo elemento con el mismo defecto es el mismo hallazgo con cualquier ancho, y la categoría es lo que mantiene separados los análisis. runs[0].properties.environment registra el viewport que usó cada ejecución.

Opciones

renderSarifReport(result, options) de @surea11y/core/sarif devuelve el registro como texto JSON. Acepta:

OpciónPor defectoEfecto
toolVersionengine.versiontool.driver.version, para una herramienta que envuelve el motor.
informationUri'https://github.com/SureA11y/core'tool.driver.informationUri, la página principal de la herramienta.
baselineEntriesningunoEl archivo de baseline tal como se guardó (su JSON.parse) o su lista entries, ver Formato del archivo. Las ocurrencias de fallo registradas ahí se omiten, como hace --baseline (ver más abajo). Una entrada escrita por la versión menor anterior, con un código de motivo que ha cambiado desde entonces, sigue coincidiendo con el hallazgo con su código nuevo (ver Actualizar de versión).
allowProfilesNotRunfalseUn análisis de varios perfiles que ejecutó menos de los que pidió (engine.profilesNotRun) contó menos de lo que se le pidió: cada perfil no ejecutado es una notificación error, y la invocación no es executionSuccessful. true las convierte en advertencias. Un valor distinto de true o false lanza un TypeError. Core 1.15.0 y posteriores.
categoryningunoruns[0].automationDetails.id, ver Analizar con varios anchos de viewport.
leanFailAs'warning''error' informa con nivel error de un hallazgo cantTell cuya tendencia apunta a fallo, para que una compilación bloqueada por errores falle también con los fallos probables. Se lee sin distinguir mayúsculas y minúsculas; cualquier otro valor lanza un TypeError. También el de una regla de buenas prácticas: una regla así, sin criterio de WCAG asociado, nunca falla, y los hechos que constata tienden a fallo sin condiciones, así que un nivel de encabezado saltado (heading-order) es un error. Un hallazgo de una regla cuyos criterios quedan todos fuera del objetivo del análisis sigue siendo una note. Un hallazgo que tiende a fallo y tiene una entrada marcada con lean: 'fail' en baselineEntries sigue siendo un warning.
leanFailWcagOnlyfalseCon leanFailAs: 'error', true eleva solo los hallazgos de reglas asociadas a un criterio de WCAG dentro del objetivo del análisis, y deja las tendencias de las reglas de buenas prácticas en warning. Un valor distinto de true o false lanza un TypeError. leaningFailures(result, { wcagOnly }), de @surea11y/core, enumera los hallazgos que elevan estas dos opciones, antes del baseline.
packsningunoLos packs que ejecutó el análisis (engineOptions.packs): en Node, los hallazgos sin decidir de una regla de un pack llevan los pasos de su propio meta.review (properties.howToCheck).

Combinación con --baseline

Un consumidor genérico de SARIF no tiene un concepto propio de "conocido, no bloquear por esto": la única forma fiel de respetar un baseline en la salida SARIF es omitir por completo las ocurrencias fail ya conocidas, en lugar de rebajarlas a warning:

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

Un baseline nunca deja fuera las ocurrencias cantTell. Un baseline escrito con buildBaselineEntries(result, { leanFail: true }) registra también los hallazgos que tienden a fallo, como entradas marcadas con lean: 'fail' (ver Baseline / allowlist); con leanFailAs: 'error' un hallazgo así se queda en warning en lugar de elevarse a error, para que un equipo pueda aceptar los fallos probables que ya tiene y seguir bloqueando los nuevos. --write-baseline solo registra ocurrencias fail.

Combinación con --html/--json

--sarif, --html y --json son indicadores de salida independientes: pasa cualquier combinación en una sola ejecución; cada uno escribe o imprime su propio informe a partir del mismo análisis único.